import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; // ===== Validators ===== /** * Bug report severity levels */ export const bugSeverityValidator = v.union( v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical") ); export type BugSeverity = "low" | "medium" | "high" | "critical"; /** * Bug report status */ export const bugStatusValidator = v.union( v.literal("open"), v.literal("in-progress"), v.literal("resolved"), v.literal("closed") ); export type BugStatus = "open" | "in-progress" | "resolved" | "closed"; /** * Feedback type */ export const feedbackTypeValidator = v.union( v.literal("feature_request"), v.literal("change_request"), v.literal("general") ); export type FeedbackType = "feature_request" | "change_request" | "general"; /** * Feedback priority */ export const feedbackPriorityValidator = v.union( v.literal("nice_to_have"), v.literal("important"), v.literal("critical") ); export type FeedbackPriority = "nice_to_have" | "important" | "critical"; /** * Feedback status */ export const feedbackStatusValidator = v.union( v.literal("open"), v.literal("under_review"), v.literal("planned"), v.literal("in_progress"), v.literal("completed"), v.literal("declined") ); export type FeedbackStatus = "open" | "under_review" | "planned" | "in_progress" | "completed" | "declined"; /** * Reporter type */ export const reporterTypeValidator = v.union( v.literal("staff"), v.literal("customer") ); export type ReporterType = "staff" | "customer"; /** * Effort estimate */ export const effortValidator = v.union( v.literal("low"), v.literal("medium"), v.literal("high") ); export type Effort = "low" | "medium" | "high"; // ===== Input Request Validators ===== /** * Input request status */ export const inputRequestStatusValidator = v.union( v.literal("pending"), v.literal("answered"), v.literal("expired"), v.literal("cancelled") ); export type InputRequestStatus = "pending" | "answered" | "expired" | "cancelled"; /** * Input type for interview */ export const inputTypeValidator = v.union( v.literal("text"), v.literal("choice"), v.literal("form") ); export type InputType = "text" | "choice" | "form"; /** * Input configuration */ export const inputConfigValidator = v.object({ placeholder: v.optional(v.string()), multiline: v.optional(v.boolean()), options: v.optional(v.array(v.object({ label: v.string(), value: v.string(), description: v.optional(v.string()), }))), fields: v.optional(v.array(v.object({ name: v.string(), label: v.string(), type: v.string(), required: v.boolean(), placeholder: v.optional(v.string()), }))), }); // ===== Counter Type Validator ===== /** * Counter type for ticket numbers */ export const counterTypeValidator = v.union( v.literal("bug"), v.literal("feedback") ); export type CounterType = "bug" | "feedback"; // ===== Schema ===== export default defineSchema({ /** * Bug Reports - in-app bug reports with diagnostics */ bugReports: defineTable({ // Report content title: v.string(), description: v.string(), severity: bugSeverityValidator, status: bugStatusValidator, // Archive flag isArchived: v.optional(v.boolean()), // Reporter info reporterType: reporterTypeValidator, reporterId: v.string(), reporterEmail: v.string(), reporterName: v.string(), // Page context url: v.string(), route: v.optional(v.string()), // Browser diagnostics browserInfo: v.string(), // JSON stringified browser info consoleErrors: v.optional(v.string()), // JSON stringified array of errors // Screenshot screenshotStorageId: v.optional(v.id("_storage")), // Viewport viewportWidth: v.number(), viewportHeight: v.number(), // Network state networkState: v.string(), // online/offline // AI Analysis fields aiSummary: v.optional(v.string()), aiRootCauseAnalysis: v.optional(v.string()), aiReproductionSteps: v.optional(v.array(v.string())), aiSuggestedFix: v.optional(v.string()), aiEstimatedEffort: v.optional(effortValidator), aiSuggestedSeverity: v.optional(bugSeverityValidator), aiAnalyzedAt: v.optional(v.number()), aiThreadId: v.optional(v.string()), notificationsSentAt: v.optional(v.number()), // Ticket number (e.g., "BUG-2025-001") ticketNumber: v.optional(v.string()), // Timestamps createdAt: v.number(), updatedAt: v.number(), }) .index("by_status", ["status"]) .index("by_severity", ["severity"]) .index("by_reporter", ["reporterType", "reporterId"]) .index("by_created", ["createdAt"]) .index("by_archived", ["isArchived"]) .index("by_ai_analyzed", ["aiAnalyzedAt"]) .index("by_ticket_number", ["ticketNumber"]), /** * Feedback - feature requests, change requests, and general feedback */ feedback: defineTable({ // Feedback type type: feedbackTypeValidator, // Content title: v.string(), description: v.string(), priority: feedbackPriorityValidator, status: feedbackStatusValidator, // Archive flag isArchived: v.optional(v.boolean()), // Reporter info reporterType: reporterTypeValidator, reporterId: v.string(), reporterEmail: v.string(), reporterName: v.string(), // Page context url: v.string(), route: v.optional(v.string()), // Browser diagnostics browserInfo: v.string(), consoleErrors: v.optional(v.string()), // Screenshot screenshotStorageId: v.optional(v.id("_storage")), // Viewport viewportWidth: v.number(), viewportHeight: v.number(), // Network state networkState: v.string(), // AI Analysis fields aiSummary: v.optional(v.string()), aiImpactAnalysis: v.optional(v.string()), aiActionItems: v.optional(v.array(v.string())), aiEstimatedEffort: v.optional(effortValidator), aiSuggestedPriority: v.optional(feedbackPriorityValidator), aiAnalyzedAt: v.optional(v.number()), aiThreadId: v.optional(v.string()), notificationsSentAt: v.optional(v.number()), // Ticket number (e.g., "FB-2025-001") ticketNumber: v.optional(v.string()), // Timestamps createdAt: v.number(), updatedAt: v.number(), }) .index("by_status", ["status"]) .index("by_type", ["type"]) .index("by_priority", ["priority"]) .index("by_reporter", ["reporterType", "reporterId"]) .index("by_created", ["createdAt"]) .index("by_archived", ["isArchived"]) .index("by_ai_analyzed", ["aiAnalyzedAt"]) .index("by_ticket_number", ["ticketNumber"]), /** * Support Teams - configure teams for notification routing */ supportTeams: defineTable({ teamName: v.string(), memberEmails: v.array(v.string()), // Multi-select: which feedback types this team handles feedbackTypes: v.array(feedbackTypeValidator), // Multi-select: which bug report severities this team handles bugReportSeverities: v.array(bugSeverityValidator), isActive: v.boolean(), createdAt: v.number(), updatedAt: v.number(), }) .index("by_active", ["isActive"]), /** * Feedback Input Requests - track pending user input during interviews */ feedbackInputRequests: defineTable({ threadId: v.string(), toolCallId: v.string(), status: inputRequestStatusValidator, inputType: inputTypeValidator, prompt: v.string(), config: v.optional(inputConfigValidator), response: v.optional(v.string()), respondedAt: v.optional(v.number()), agentName: v.string(), userId: v.optional(v.string()), createdAt: v.number(), expiresAt: v.optional(v.number()), }) .index("by_thread_status", ["threadId", "status"]) .index("by_user_status", ["userId", "status"]) .index("by_created", ["createdAt"]), /** * Interview Sessions - track interview state and generated reports */ interviewSessions: defineTable({ threadId: v.string(), reportType: v.union(v.literal("bug"), v.literal("feedback")), // Reporter info reporterType: reporterTypeValidator, reporterId: v.string(), reporterEmail: v.string(), reporterName: v.string(), // Interview context (JSON stringified) context: v.optional(v.string()), // Generated report generatedTitle: v.optional(v.string()), generatedDescription: v.optional(v.string()), generatedSeverity: v.optional(bugSeverityValidator), generatedFeedbackType: v.optional(feedbackTypeValidator), generatedPriority: v.optional(feedbackPriorityValidator), generatedFeatureArea: v.optional(v.string()), generatedReproSteps: v.optional(v.array(v.string())), // State isComplete: v.boolean(), createdAt: v.number(), updatedAt: v.number(), }) .index("by_thread", ["threadId"]) .index("by_reporter", ["reporterType", "reporterId"]) .index("by_created", ["createdAt"]), /** * Counters - track sequential numbers for ticket IDs */ ticketCounters: defineTable({ type: counterTypeValidator, year: v.number(), nextNumber: v.number(), }) .index("by_type_year", ["type", "year"]), /** * API Keys - for accessing the HTTP endpoint */ apiKeys: defineTable({ // The hashed key (SHA-256) keyHash: v.string(), // First 8 chars of the key for identification (e.g., "fb_abc123") keyPrefix: v.string(), // Human-readable name name: v.string(), // Optional expiration expiresAt: v.optional(v.number()), // Revocation status isRevoked: v.boolean(), revokedAt: v.optional(v.number()), // Timestamps createdAt: v.number(), }) .index("by_key_hash", ["keyHash"]) .index("by_key_prefix", ["keyPrefix"]) .index("by_revoked", ["isRevoked"]), });