@fatagnus/convex-feedback 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +382 -0
  3. package/dist/convex/agents/bugReportAgent.d.ts +30 -0
  4. package/dist/convex/agents/bugReportAgent.d.ts.map +1 -0
  5. package/dist/convex/agents/bugReportAgent.js +243 -0
  6. package/dist/convex/agents/bugReportAgent.js.map +1 -0
  7. package/dist/convex/agents/feedbackAgent.d.ts +29 -0
  8. package/dist/convex/agents/feedbackAgent.d.ts.map +1 -0
  9. package/dist/convex/agents/feedbackAgent.js +232 -0
  10. package/dist/convex/agents/feedbackAgent.js.map +1 -0
  11. package/dist/convex/bugReports.d.ts +49 -0
  12. package/dist/convex/bugReports.d.ts.map +1 -0
  13. package/dist/convex/bugReports.js +321 -0
  14. package/dist/convex/bugReports.js.map +1 -0
  15. package/dist/convex/convex.config.d.ts +3 -0
  16. package/dist/convex/convex.config.d.ts.map +1 -0
  17. package/dist/convex/convex.config.js +6 -0
  18. package/dist/convex/convex.config.js.map +1 -0
  19. package/dist/convex/emails/bugReportEmails.d.ts +16 -0
  20. package/dist/convex/emails/bugReportEmails.d.ts.map +1 -0
  21. package/dist/convex/emails/bugReportEmails.js +403 -0
  22. package/dist/convex/emails/bugReportEmails.js.map +1 -0
  23. package/dist/convex/emails/feedbackEmails.d.ts +16 -0
  24. package/dist/convex/emails/feedbackEmails.d.ts.map +1 -0
  25. package/dist/convex/emails/feedbackEmails.js +389 -0
  26. package/dist/convex/emails/feedbackEmails.js.map +1 -0
  27. package/dist/convex/feedback.d.ts +49 -0
  28. package/dist/convex/feedback.d.ts.map +1 -0
  29. package/dist/convex/feedback.js +327 -0
  30. package/dist/convex/feedback.js.map +1 -0
  31. package/dist/convex/index.d.ts +10 -0
  32. package/dist/convex/index.d.ts.map +1 -0
  33. package/dist/convex/index.js +12 -0
  34. package/dist/convex/index.js.map +1 -0
  35. package/dist/convex/schema.d.ts +200 -0
  36. package/dist/convex/schema.d.ts.map +1 -0
  37. package/dist/convex/schema.js +150 -0
  38. package/dist/convex/schema.js.map +1 -0
  39. package/dist/convex/supportTeams.d.ts +29 -0
  40. package/dist/convex/supportTeams.d.ts.map +1 -0
  41. package/dist/convex/supportTeams.js +159 -0
  42. package/dist/convex/supportTeams.js.map +1 -0
  43. package/dist/index.d.ts +70 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +63 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/react/BugReportButton.d.ts +70 -0
  48. package/dist/react/BugReportButton.d.ts.map +1 -0
  49. package/dist/react/BugReportButton.js +371 -0
  50. package/dist/react/BugReportButton.js.map +1 -0
  51. package/dist/react/BugReportContext.d.ts +59 -0
  52. package/dist/react/BugReportContext.d.ts.map +1 -0
  53. package/dist/react/BugReportContext.js +107 -0
  54. package/dist/react/BugReportContext.js.map +1 -0
  55. package/dist/react/index.d.ts +36 -0
  56. package/dist/react/index.d.ts.map +1 -0
  57. package/dist/react/index.js +36 -0
  58. package/dist/react/index.js.map +1 -0
  59. package/dist/types.d.ts +89 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +5 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +101 -0
  64. package/src/convex/agents/bugReportAgent.ts +277 -0
  65. package/src/convex/agents/feedbackAgent.ts +264 -0
  66. package/src/convex/bugReports.ts +350 -0
  67. package/src/convex/convex.config.ts +7 -0
  68. package/src/convex/emails/bugReportEmails.ts +479 -0
  69. package/src/convex/emails/feedbackEmails.ts +465 -0
  70. package/src/convex/feedback.ts +356 -0
  71. package/src/convex/index.ts +28 -0
  72. package/src/convex/schema.ts +207 -0
  73. package/src/convex/supportTeams.ts +179 -0
  74. package/src/index.ts +77 -0
  75. package/src/react/BugReportButton.tsx +755 -0
  76. package/src/react/BugReportContext.tsx +146 -0
  77. package/src/react/index.ts +46 -0
  78. package/src/types.ts +93 -0
@@ -0,0 +1,356 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query, internalMutation } from "./_generated/server";
3
+ import { internal } from "./_generated/api";
4
+ import {
5
+ feedbackTypeValidator,
6
+ feedbackPriorityValidator,
7
+ feedbackStatusValidator,
8
+ reporterTypeValidator,
9
+ effortValidator,
10
+ } from "./schema";
11
+
12
+ // Return type for feedback records
13
+ const feedbackReturnValidator = v.object({
14
+ _id: v.id("feedback"),
15
+ _creationTime: v.number(),
16
+ type: feedbackTypeValidator,
17
+ title: v.string(),
18
+ description: v.string(),
19
+ priority: feedbackPriorityValidator,
20
+ status: feedbackStatusValidator,
21
+ isArchived: v.optional(v.boolean()),
22
+ reporterType: reporterTypeValidator,
23
+ reporterId: v.string(),
24
+ reporterEmail: v.string(),
25
+ reporterName: v.string(),
26
+ url: v.string(),
27
+ route: v.optional(v.string()),
28
+ browserInfo: v.string(),
29
+ consoleErrors: v.optional(v.string()),
30
+ screenshotStorageId: v.optional(v.id("_storage")),
31
+ viewportWidth: v.number(),
32
+ viewportHeight: v.number(),
33
+ networkState: v.string(),
34
+ createdAt: v.number(),
35
+ updatedAt: v.number(),
36
+ // AI Analysis fields
37
+ aiSummary: v.optional(v.string()),
38
+ aiImpactAnalysis: v.optional(v.string()),
39
+ aiActionItems: v.optional(v.array(v.string())),
40
+ aiEstimatedEffort: v.optional(effortValidator),
41
+ aiSuggestedPriority: v.optional(feedbackPriorityValidator),
42
+ aiAnalyzedAt: v.optional(v.number()),
43
+ aiThreadId: v.optional(v.string()),
44
+ notificationsSentAt: v.optional(v.number()),
45
+ });
46
+
47
+ /**
48
+ * Create new feedback.
49
+ * This mutation is intentionally public (no auth required) because:
50
+ * 1. We want to capture feedback even when auth might be broken
51
+ * 2. The reporter info is passed explicitly from the client
52
+ */
53
+ export const create = mutation({
54
+ args: {
55
+ type: feedbackTypeValidator,
56
+ title: v.string(),
57
+ description: v.string(),
58
+ priority: feedbackPriorityValidator,
59
+ reporterType: reporterTypeValidator,
60
+ reporterId: v.string(),
61
+ reporterEmail: v.string(),
62
+ reporterName: v.string(),
63
+ url: v.string(),
64
+ route: v.optional(v.string()),
65
+ browserInfo: v.string(),
66
+ consoleErrors: v.optional(v.string()),
67
+ screenshotStorageId: v.optional(v.id("_storage")),
68
+ viewportWidth: v.number(),
69
+ viewportHeight: v.number(),
70
+ networkState: v.string(),
71
+ },
72
+ returns: v.id("feedback"),
73
+ handler: async (ctx, args) => {
74
+ const now = Date.now();
75
+
76
+ const feedbackId = await ctx.db.insert("feedback", {
77
+ type: args.type,
78
+ title: args.title,
79
+ description: args.description,
80
+ priority: args.priority,
81
+ status: "open",
82
+ reporterType: args.reporterType,
83
+ reporterId: args.reporterId,
84
+ reporterEmail: args.reporterEmail,
85
+ reporterName: args.reporterName,
86
+ url: args.url,
87
+ route: args.route,
88
+ browserInfo: args.browserInfo,
89
+ consoleErrors: args.consoleErrors,
90
+ screenshotStorageId: args.screenshotStorageId,
91
+ viewportWidth: args.viewportWidth,
92
+ viewportHeight: args.viewportHeight,
93
+ networkState: args.networkState,
94
+ createdAt: now,
95
+ updatedAt: now,
96
+ });
97
+
98
+ // Schedule AI analysis and email notifications (runs immediately)
99
+ try {
100
+ await ctx.scheduler.runAfter(0, internal.agents.feedbackAgent.processFeedback, {
101
+ feedbackId,
102
+ });
103
+ } catch {
104
+ // AI agent not available, continue without analysis
105
+ console.log("AI agent not configured, skipping analysis");
106
+ }
107
+
108
+ return feedbackId;
109
+ },
110
+ });
111
+
112
+ /**
113
+ * List feedback with optional filters.
114
+ * By default, archived items are excluded unless includeArchived is true.
115
+ */
116
+ export const list = query({
117
+ args: {
118
+ status: v.optional(feedbackStatusValidator),
119
+ type: v.optional(feedbackTypeValidator),
120
+ priority: v.optional(feedbackPriorityValidator),
121
+ includeArchived: v.optional(v.boolean()),
122
+ limit: v.optional(v.number()),
123
+ },
124
+ returns: v.array(feedbackReturnValidator),
125
+ handler: async (ctx, args) => {
126
+ const limit = args.limit ?? 50;
127
+ const includeArchived = args.includeArchived ?? false;
128
+
129
+ let results;
130
+ if (args.status) {
131
+ const status = args.status;
132
+ results = await ctx.db
133
+ .query("feedback")
134
+ .withIndex("by_status", (q) => q.eq("status", status))
135
+ .order("desc")
136
+ .take(limit * 2);
137
+ } else if (args.type) {
138
+ const type = args.type;
139
+ results = await ctx.db
140
+ .query("feedback")
141
+ .withIndex("by_type", (q) => q.eq("type", type))
142
+ .order("desc")
143
+ .take(limit * 2);
144
+ } else if (args.priority) {
145
+ const priority = args.priority;
146
+ results = await ctx.db
147
+ .query("feedback")
148
+ .withIndex("by_priority", (q) => q.eq("priority", priority))
149
+ .order("desc")
150
+ .take(limit * 2);
151
+ } else {
152
+ results = await ctx.db
153
+ .query("feedback")
154
+ .withIndex("by_created")
155
+ .order("desc")
156
+ .take(limit * 2);
157
+ }
158
+
159
+ // Filter out archived items unless includeArchived is true
160
+ if (!includeArchived) {
161
+ results = results.filter((r) => !r.isArchived);
162
+ }
163
+
164
+ return results.slice(0, limit);
165
+ },
166
+ });
167
+
168
+ /**
169
+ * Get a single feedback by ID.
170
+ */
171
+ export const get = query({
172
+ args: {
173
+ feedbackId: v.id("feedback"),
174
+ },
175
+ returns: v.union(feedbackReturnValidator, v.null()),
176
+ handler: async (ctx, args) => {
177
+ return await ctx.db.get(args.feedbackId);
178
+ },
179
+ });
180
+
181
+ /**
182
+ * Update feedback status.
183
+ */
184
+ export const updateStatus = mutation({
185
+ args: {
186
+ feedbackId: v.id("feedback"),
187
+ status: feedbackStatusValidator,
188
+ },
189
+ returns: v.null(),
190
+ handler: async (ctx, args) => {
191
+ const feedback = await ctx.db.get(args.feedbackId);
192
+ if (!feedback) {
193
+ throw new Error("Feedback not found");
194
+ }
195
+
196
+ await ctx.db.patch(args.feedbackId, {
197
+ status: args.status,
198
+ updatedAt: Date.now(),
199
+ });
200
+
201
+ return null;
202
+ },
203
+ });
204
+
205
+ /**
206
+ * Generate upload URL for screenshot.
207
+ */
208
+ export const generateUploadUrl = mutation({
209
+ args: {},
210
+ returns: v.string(),
211
+ handler: async (ctx) => {
212
+ return await ctx.storage.generateUploadUrl();
213
+ },
214
+ });
215
+
216
+ /**
217
+ * Get screenshot URL from storage ID.
218
+ */
219
+ export const getScreenshotUrl = query({
220
+ args: {
221
+ storageId: v.id("_storage"),
222
+ },
223
+ returns: v.union(v.string(), v.null()),
224
+ handler: async (ctx, args) => {
225
+ return await ctx.storage.getUrl(args.storageId);
226
+ },
227
+ });
228
+
229
+ /**
230
+ * Archive feedback.
231
+ */
232
+ export const archive = mutation({
233
+ args: {
234
+ feedbackId: v.id("feedback"),
235
+ },
236
+ returns: v.null(),
237
+ handler: async (ctx, args) => {
238
+ const feedback = await ctx.db.get(args.feedbackId);
239
+ if (!feedback) {
240
+ throw new Error("Feedback not found");
241
+ }
242
+
243
+ await ctx.db.patch(args.feedbackId, {
244
+ isArchived: true,
245
+ updatedAt: Date.now(),
246
+ });
247
+
248
+ return null;
249
+ },
250
+ });
251
+
252
+ /**
253
+ * Unarchive feedback.
254
+ */
255
+ export const unarchive = mutation({
256
+ args: {
257
+ feedbackId: v.id("feedback"),
258
+ },
259
+ returns: v.null(),
260
+ handler: async (ctx, args) => {
261
+ const feedback = await ctx.db.get(args.feedbackId);
262
+ if (!feedback) {
263
+ throw new Error("Feedback not found");
264
+ }
265
+
266
+ await ctx.db.patch(args.feedbackId, {
267
+ isArchived: false,
268
+ updatedAt: Date.now(),
269
+ });
270
+
271
+ return null;
272
+ },
273
+ });
274
+
275
+ /**
276
+ * Get AI analysis for a specific feedback.
277
+ */
278
+ export const getAiAnalysis = query({
279
+ args: {
280
+ feedbackId: v.id("feedback"),
281
+ },
282
+ returns: v.union(
283
+ v.object({
284
+ aiSummary: v.union(v.string(), v.null()),
285
+ aiImpactAnalysis: v.union(v.string(), v.null()),
286
+ aiActionItems: v.union(v.array(v.string()), v.null()),
287
+ aiEstimatedEffort: v.union(effortValidator, v.null()),
288
+ aiSuggestedPriority: v.union(feedbackPriorityValidator, v.null()),
289
+ aiAnalyzedAt: v.union(v.number(), v.null()),
290
+ notificationsSentAt: v.union(v.number(), v.null()),
291
+ }),
292
+ v.null()
293
+ ),
294
+ handler: async (ctx, args) => {
295
+ const feedback = await ctx.db.get(args.feedbackId);
296
+ if (!feedback) {
297
+ return null;
298
+ }
299
+
300
+ return {
301
+ aiSummary: feedback.aiSummary ?? null,
302
+ aiImpactAnalysis: feedback.aiImpactAnalysis ?? null,
303
+ aiActionItems: feedback.aiActionItems ?? null,
304
+ aiEstimatedEffort: feedback.aiEstimatedEffort ?? null,
305
+ aiSuggestedPriority: feedback.aiSuggestedPriority ?? null,
306
+ aiAnalyzedAt: feedback.aiAnalyzedAt ?? null,
307
+ notificationsSentAt: feedback.notificationsSentAt ?? null,
308
+ };
309
+ },
310
+ });
311
+
312
+ /**
313
+ * Internal mutation to update feedback with AI analysis results
314
+ */
315
+ export const updateWithAnalysis = internalMutation({
316
+ args: {
317
+ feedbackId: v.id("feedback"),
318
+ aiSummary: v.string(),
319
+ aiImpactAnalysis: v.string(),
320
+ aiActionItems: v.array(v.string()),
321
+ aiEstimatedEffort: effortValidator,
322
+ aiSuggestedPriority: feedbackPriorityValidator,
323
+ aiThreadId: v.string(),
324
+ },
325
+ returns: v.null(),
326
+ handler: async (ctx, args) => {
327
+ await ctx.db.patch(args.feedbackId, {
328
+ aiSummary: args.aiSummary,
329
+ aiImpactAnalysis: args.aiImpactAnalysis,
330
+ aiActionItems: args.aiActionItems,
331
+ aiEstimatedEffort: args.aiEstimatedEffort,
332
+ aiSuggestedPriority: args.aiSuggestedPriority,
333
+ aiThreadId: args.aiThreadId,
334
+ aiAnalyzedAt: Date.now(),
335
+ updatedAt: Date.now(),
336
+ });
337
+ return null;
338
+ },
339
+ });
340
+
341
+ /**
342
+ * Internal mutation to mark notifications as sent
343
+ */
344
+ export const markNotificationsSent = internalMutation({
345
+ args: {
346
+ feedbackId: v.id("feedback"),
347
+ },
348
+ returns: v.null(),
349
+ handler: async (ctx, args) => {
350
+ await ctx.db.patch(args.feedbackId, {
351
+ notificationsSentAt: Date.now(),
352
+ updatedAt: Date.now(),
353
+ });
354
+ return null;
355
+ },
356
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @convex-dev/feedback - Convex Functions
3
+ *
4
+ * Export all Convex functions for use by the component.
5
+ */
6
+
7
+ // Re-export all public functions
8
+ export * as bugReports from './bugReports';
9
+ export * as feedback from './feedback';
10
+ export * as supportTeams from './supportTeams';
11
+
12
+ // Re-export schema types and validators
13
+ export {
14
+ bugSeverityValidator,
15
+ bugStatusValidator,
16
+ feedbackTypeValidator,
17
+ feedbackPriorityValidator,
18
+ feedbackStatusValidator,
19
+ reporterTypeValidator,
20
+ effortValidator,
21
+ type BugSeverity,
22
+ type BugStatus,
23
+ type FeedbackType,
24
+ type FeedbackPriority,
25
+ type FeedbackStatus,
26
+ type ReporterType,
27
+ type Effort,
28
+ } from './schema';
@@ -0,0 +1,207 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+
4
+ // ===== Validators =====
5
+
6
+ /**
7
+ * Bug report severity levels
8
+ */
9
+ export const bugSeverityValidator = v.union(
10
+ v.literal("low"),
11
+ v.literal("medium"),
12
+ v.literal("high"),
13
+ v.literal("critical")
14
+ );
15
+
16
+ export type BugSeverity = "low" | "medium" | "high" | "critical";
17
+
18
+ /**
19
+ * Bug report status
20
+ */
21
+ export const bugStatusValidator = v.union(
22
+ v.literal("open"),
23
+ v.literal("in-progress"),
24
+ v.literal("resolved"),
25
+ v.literal("closed")
26
+ );
27
+
28
+ export type BugStatus = "open" | "in-progress" | "resolved" | "closed";
29
+
30
+ /**
31
+ * Feedback type
32
+ */
33
+ export const feedbackTypeValidator = v.union(
34
+ v.literal("feature_request"),
35
+ v.literal("change_request"),
36
+ v.literal("general")
37
+ );
38
+
39
+ export type FeedbackType = "feature_request" | "change_request" | "general";
40
+
41
+ /**
42
+ * Feedback priority
43
+ */
44
+ export const feedbackPriorityValidator = v.union(
45
+ v.literal("nice_to_have"),
46
+ v.literal("important"),
47
+ v.literal("critical")
48
+ );
49
+
50
+ export type FeedbackPriority = "nice_to_have" | "important" | "critical";
51
+
52
+ /**
53
+ * Feedback status
54
+ */
55
+ export const feedbackStatusValidator = v.union(
56
+ v.literal("open"),
57
+ v.literal("under_review"),
58
+ v.literal("planned"),
59
+ v.literal("in_progress"),
60
+ v.literal("completed"),
61
+ v.literal("declined")
62
+ );
63
+
64
+ export type FeedbackStatus = "open" | "under_review" | "planned" | "in_progress" | "completed" | "declined";
65
+
66
+ /**
67
+ * Reporter type
68
+ */
69
+ export const reporterTypeValidator = v.union(
70
+ v.literal("staff"),
71
+ v.literal("customer")
72
+ );
73
+
74
+ export type ReporterType = "staff" | "customer";
75
+
76
+ /**
77
+ * Effort estimate
78
+ */
79
+ export const effortValidator = v.union(
80
+ v.literal("low"),
81
+ v.literal("medium"),
82
+ v.literal("high")
83
+ );
84
+
85
+ export type Effort = "low" | "medium" | "high";
86
+
87
+ // ===== Schema =====
88
+
89
+ export default defineSchema({
90
+ /**
91
+ * Bug Reports - in-app bug reports with diagnostics
92
+ */
93
+ bugReports: defineTable({
94
+ // Report content
95
+ title: v.string(),
96
+ description: v.string(),
97
+ severity: bugSeverityValidator,
98
+ status: bugStatusValidator,
99
+ // Archive flag
100
+ isArchived: v.optional(v.boolean()),
101
+ // Reporter info
102
+ reporterType: reporterTypeValidator,
103
+ reporterId: v.string(),
104
+ reporterEmail: v.string(),
105
+ reporterName: v.string(),
106
+ // Page context
107
+ url: v.string(),
108
+ route: v.optional(v.string()),
109
+ // Browser diagnostics
110
+ browserInfo: v.string(), // JSON stringified browser info
111
+ consoleErrors: v.optional(v.string()), // JSON stringified array of errors
112
+ // Screenshot
113
+ screenshotStorageId: v.optional(v.id("_storage")),
114
+ // Viewport
115
+ viewportWidth: v.number(),
116
+ viewportHeight: v.number(),
117
+ // Network state
118
+ networkState: v.string(), // online/offline
119
+ // AI Analysis fields
120
+ aiSummary: v.optional(v.string()),
121
+ aiRootCauseAnalysis: v.optional(v.string()),
122
+ aiReproductionSteps: v.optional(v.array(v.string())),
123
+ aiSuggestedFix: v.optional(v.string()),
124
+ aiEstimatedEffort: v.optional(effortValidator),
125
+ aiSuggestedSeverity: v.optional(bugSeverityValidator),
126
+ aiAnalyzedAt: v.optional(v.number()),
127
+ aiThreadId: v.optional(v.string()),
128
+ notificationsSentAt: v.optional(v.number()),
129
+ // Timestamps
130
+ createdAt: v.number(),
131
+ updatedAt: v.number(),
132
+ })
133
+ .index("by_status", ["status"])
134
+ .index("by_severity", ["severity"])
135
+ .index("by_reporter", ["reporterType", "reporterId"])
136
+ .index("by_created", ["createdAt"])
137
+ .index("by_archived", ["isArchived"])
138
+ .index("by_ai_analyzed", ["aiAnalyzedAt"]),
139
+
140
+ /**
141
+ * Feedback - feature requests, change requests, and general feedback
142
+ */
143
+ feedback: defineTable({
144
+ // Feedback type
145
+ type: feedbackTypeValidator,
146
+ // Content
147
+ title: v.string(),
148
+ description: v.string(),
149
+ priority: feedbackPriorityValidator,
150
+ status: feedbackStatusValidator,
151
+ // Archive flag
152
+ isArchived: v.optional(v.boolean()),
153
+ // Reporter info
154
+ reporterType: reporterTypeValidator,
155
+ reporterId: v.string(),
156
+ reporterEmail: v.string(),
157
+ reporterName: v.string(),
158
+ // Page context
159
+ url: v.string(),
160
+ route: v.optional(v.string()),
161
+ // Browser diagnostics
162
+ browserInfo: v.string(),
163
+ consoleErrors: v.optional(v.string()),
164
+ // Screenshot
165
+ screenshotStorageId: v.optional(v.id("_storage")),
166
+ // Viewport
167
+ viewportWidth: v.number(),
168
+ viewportHeight: v.number(),
169
+ // Network state
170
+ networkState: v.string(),
171
+ // AI Analysis fields
172
+ aiSummary: v.optional(v.string()),
173
+ aiImpactAnalysis: v.optional(v.string()),
174
+ aiActionItems: v.optional(v.array(v.string())),
175
+ aiEstimatedEffort: v.optional(effortValidator),
176
+ aiSuggestedPriority: v.optional(feedbackPriorityValidator),
177
+ aiAnalyzedAt: v.optional(v.number()),
178
+ aiThreadId: v.optional(v.string()),
179
+ notificationsSentAt: v.optional(v.number()),
180
+ // Timestamps
181
+ createdAt: v.number(),
182
+ updatedAt: v.number(),
183
+ })
184
+ .index("by_status", ["status"])
185
+ .index("by_type", ["type"])
186
+ .index("by_priority", ["priority"])
187
+ .index("by_reporter", ["reporterType", "reporterId"])
188
+ .index("by_created", ["createdAt"])
189
+ .index("by_archived", ["isArchived"])
190
+ .index("by_ai_analyzed", ["aiAnalyzedAt"]),
191
+
192
+ /**
193
+ * Support Teams - configure teams for notification routing
194
+ */
195
+ supportTeams: defineTable({
196
+ teamName: v.string(),
197
+ memberEmails: v.array(v.string()),
198
+ // Multi-select: which feedback types this team handles
199
+ feedbackTypes: v.array(feedbackTypeValidator),
200
+ // Multi-select: which bug report severities this team handles
201
+ bugReportSeverities: v.array(bugSeverityValidator),
202
+ isActive: v.boolean(),
203
+ createdAt: v.number(),
204
+ updatedAt: v.number(),
205
+ })
206
+ .index("by_active", ["isActive"]),
207
+ });