@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.
- package/LICENSE +177 -0
- package/README.md +382 -0
- package/dist/convex/agents/bugReportAgent.d.ts +30 -0
- package/dist/convex/agents/bugReportAgent.d.ts.map +1 -0
- package/dist/convex/agents/bugReportAgent.js +243 -0
- package/dist/convex/agents/bugReportAgent.js.map +1 -0
- package/dist/convex/agents/feedbackAgent.d.ts +29 -0
- package/dist/convex/agents/feedbackAgent.d.ts.map +1 -0
- package/dist/convex/agents/feedbackAgent.js +232 -0
- package/dist/convex/agents/feedbackAgent.js.map +1 -0
- package/dist/convex/bugReports.d.ts +49 -0
- package/dist/convex/bugReports.d.ts.map +1 -0
- package/dist/convex/bugReports.js +321 -0
- package/dist/convex/bugReports.js.map +1 -0
- package/dist/convex/convex.config.d.ts +3 -0
- package/dist/convex/convex.config.d.ts.map +1 -0
- package/dist/convex/convex.config.js +6 -0
- package/dist/convex/convex.config.js.map +1 -0
- package/dist/convex/emails/bugReportEmails.d.ts +16 -0
- package/dist/convex/emails/bugReportEmails.d.ts.map +1 -0
- package/dist/convex/emails/bugReportEmails.js +403 -0
- package/dist/convex/emails/bugReportEmails.js.map +1 -0
- package/dist/convex/emails/feedbackEmails.d.ts +16 -0
- package/dist/convex/emails/feedbackEmails.d.ts.map +1 -0
- package/dist/convex/emails/feedbackEmails.js +389 -0
- package/dist/convex/emails/feedbackEmails.js.map +1 -0
- package/dist/convex/feedback.d.ts +49 -0
- package/dist/convex/feedback.d.ts.map +1 -0
- package/dist/convex/feedback.js +327 -0
- package/dist/convex/feedback.js.map +1 -0
- package/dist/convex/index.d.ts +10 -0
- package/dist/convex/index.d.ts.map +1 -0
- package/dist/convex/index.js +12 -0
- package/dist/convex/index.js.map +1 -0
- package/dist/convex/schema.d.ts +200 -0
- package/dist/convex/schema.d.ts.map +1 -0
- package/dist/convex/schema.js +150 -0
- package/dist/convex/schema.js.map +1 -0
- package/dist/convex/supportTeams.d.ts +29 -0
- package/dist/convex/supportTeams.d.ts.map +1 -0
- package/dist/convex/supportTeams.js +159 -0
- package/dist/convex/supportTeams.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/react/BugReportButton.d.ts +70 -0
- package/dist/react/BugReportButton.d.ts.map +1 -0
- package/dist/react/BugReportButton.js +371 -0
- package/dist/react/BugReportButton.js.map +1 -0
- package/dist/react/BugReportContext.d.ts +59 -0
- package/dist/react/BugReportContext.d.ts.map +1 -0
- package/dist/react/BugReportContext.js +107 -0
- package/dist/react/BugReportContext.js.map +1 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +36 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +101 -0
- package/src/convex/agents/bugReportAgent.ts +277 -0
- package/src/convex/agents/feedbackAgent.ts +264 -0
- package/src/convex/bugReports.ts +350 -0
- package/src/convex/convex.config.ts +7 -0
- package/src/convex/emails/bugReportEmails.ts +479 -0
- package/src/convex/emails/feedbackEmails.ts +465 -0
- package/src/convex/feedback.ts +356 -0
- package/src/convex/index.ts +28 -0
- package/src/convex/schema.ts +207 -0
- package/src/convex/supportTeams.ts +179 -0
- package/src/index.ts +77 -0
- package/src/react/BugReportButton.tsx +755 -0
- package/src/react/BugReportContext.tsx +146 -0
- package/src/react/index.ts +46 -0
- 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
|
+
});
|