@alliance-droid/status-feedback-system 0.1.0 → 0.2.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 (54) hide show
  1. package/dist/api.d.ts +94 -0
  2. package/dist/api.js +100 -0
  3. package/dist/components/FeedbackBoard.svelte +842 -0
  4. package/dist/components/FeedbackBoard.svelte.d.ts +19 -0
  5. package/dist/components/FeedbackButton.svelte +3 -3
  6. package/dist/components/FeedbackButton.svelte.d.ts +1 -1
  7. package/dist/components/FeedbackForm.svelte +3 -3
  8. package/dist/components/FeedbackForm.svelte.d.ts +1 -1
  9. package/dist/components/StatusBanner.svelte +36 -3
  10. package/dist/components/StatusBanner.svelte.d.ts +7 -1
  11. package/dist/components/StatusPage.svelte +150 -83
  12. package/dist/components/StatusPage.svelte.d.ts +10 -3
  13. package/dist/index.d.ts +7 -9
  14. package/dist/index.js +6 -8
  15. package/dist/types.d.ts +13 -6
  16. package/package.json +2 -13
  17. package/dist/components/admin/AdminDashboard.svelte +0 -274
  18. package/dist/components/admin/AdminDashboard.svelte.d.ts +0 -9
  19. package/dist/components/admin/ApiKeyManager.svelte +0 -68
  20. package/dist/components/admin/ApiKeyManager.svelte.d.ts +0 -7
  21. package/dist/components/admin/FeedbackList.svelte +0 -160
  22. package/dist/components/admin/FeedbackList.svelte.d.ts +0 -17
  23. package/dist/components/admin/IncidentForm.svelte +0 -135
  24. package/dist/components/admin/IncidentForm.svelte.d.ts +0 -14
  25. package/dist/components/admin/IncidentManager.svelte +0 -153
  26. package/dist/components/admin/IncidentManager.svelte.d.ts +0 -13
  27. package/dist/components/admin/ProjectForm.svelte +0 -106
  28. package/dist/components/admin/ProjectForm.svelte.d.ts +0 -13
  29. package/dist/components/admin/ProjectList.svelte +0 -57
  30. package/dist/components/admin/ProjectList.svelte.d.ts +0 -10
  31. package/dist/components/admin/StatusControl.svelte +0 -62
  32. package/dist/components/admin/StatusControl.svelte.d.ts +0 -8
  33. package/dist/components/admin/SystemManager.svelte +0 -119
  34. package/dist/components/admin/SystemManager.svelte.d.ts +0 -13
  35. package/dist/components/admin/index.d.ts +0 -9
  36. package/dist/components/admin/index.js +0 -9
  37. package/dist/server/db.d.ts +0 -17
  38. package/dist/server/db.js +0 -68
  39. package/dist/server/feedback.d.ts +0 -27
  40. package/dist/server/feedback.js +0 -124
  41. package/dist/server/handlers.d.ts +0 -41
  42. package/dist/server/handlers.js +0 -251
  43. package/dist/server/incidents.d.ts +0 -27
  44. package/dist/server/incidents.js +0 -180
  45. package/dist/server/index.d.ts +0 -101
  46. package/dist/server/index.js +0 -71
  47. package/dist/server/projects.d.ts +0 -45
  48. package/dist/server/projects.js +0 -156
  49. package/dist/server/schema.d.ts +0 -9
  50. package/dist/server/schema.js +0 -94
  51. package/dist/server/systems.d.ts +0 -20
  52. package/dist/server/systems.js +0 -146
  53. package/dist/server/types.d.ts +0 -4
  54. package/dist/server/types.js +0 -4
@@ -1,27 +0,0 @@
1
- /**
2
- * Feedback CRUD
3
- */
4
- import type { ConnectionPool } from 'mssql';
5
- import type { Feedback, FeedbackStatus } from './types.js';
6
- export declare function submitFeedback(pool: ConnectionPool, projectId: string, data: {
7
- category?: string;
8
- message: string;
9
- email?: string;
10
- userAgent?: string;
11
- pageUrl?: string;
12
- metadata?: Record<string, unknown>;
13
- }): Promise<Feedback>;
14
- export declare function getFeedbackById(pool: ConnectionPool, id: string): Promise<Feedback | null>;
15
- export declare function getFeedbackByProject(pool: ConnectionPool, projectId: string, options?: {
16
- limit?: number;
17
- offset?: number;
18
- category?: string;
19
- status?: FeedbackStatus;
20
- sortBy?: 'created_at' | 'status';
21
- sortOrder?: 'ASC' | 'DESC';
22
- }): Promise<{
23
- items: Feedback[];
24
- total: number;
25
- }>;
26
- export declare function updateFeedbackStatus(pool: ConnectionPool, id: string, status: FeedbackStatus): Promise<Feedback | null>;
27
- export declare function deleteFeedback(pool: ConnectionPool, id: string): Promise<boolean>;
@@ -1,124 +0,0 @@
1
- /**
2
- * Feedback CRUD
3
- */
4
- import { nanoid } from 'nanoid';
5
- // ── CRUD ──
6
- export async function submitFeedback(pool, projectId, data) {
7
- const id = nanoid(16);
8
- const now = Date.now();
9
- await pool
10
- .request()
11
- .input('id', id)
12
- .input('projectId', projectId)
13
- .input('category', data.category || null)
14
- .input('message', data.message)
15
- .input('email', data.email || null)
16
- .input('userAgent', data.userAgent || null)
17
- .input('pageUrl', data.pageUrl || null)
18
- .input('metadata', data.metadata ? JSON.stringify(data.metadata) : null)
19
- .input('createdAt', now)
20
- .query(`
21
- INSERT INTO sf_feedback (id, project_id, category, message, email, user_agent, page_url, metadata, created_at)
22
- VALUES (@id, @projectId, @category, @message, @email, @userAgent, @pageUrl, @metadata, @createdAt)
23
- `);
24
- return {
25
- id,
26
- projectId,
27
- category: data.category || null,
28
- message: data.message,
29
- email: data.email || null,
30
- userAgent: data.userAgent || null,
31
- pageUrl: data.pageUrl || null,
32
- metadata: data.metadata || null,
33
- status: 'new',
34
- createdAt: now,
35
- };
36
- }
37
- export async function getFeedbackById(pool, id) {
38
- const result = await pool
39
- .request()
40
- .input('id', id)
41
- .query('SELECT * FROM sf_feedback WHERE id = @id');
42
- if (result.recordset.length === 0)
43
- return null;
44
- return mapFeedbackRow(result.recordset[0]);
45
- }
46
- export async function getFeedbackByProject(pool, projectId, options = {}) {
47
- const limit = options.limit ?? 50;
48
- const offset = options.offset ?? 0;
49
- const sortBy = options.sortBy ?? 'created_at';
50
- const sortOrder = options.sortOrder ?? 'DESC';
51
- // Build WHERE clause
52
- const conditions = ['project_id = @projectId'];
53
- if (options.category)
54
- conditions.push('category = @category');
55
- if (options.status)
56
- conditions.push('status = @status');
57
- const where = conditions.join(' AND ');
58
- // Get total count
59
- const countResult = await pool
60
- .request()
61
- .input('projectId', projectId)
62
- .input('category', options.category || null)
63
- .input('status', options.status || null)
64
- .query(`SELECT COUNT(*) as total FROM sf_feedback WHERE ${where}`);
65
- const total = countResult.recordset[0].total;
66
- // Get paginated results
67
- const result = await pool
68
- .request()
69
- .input('projectId', projectId)
70
- .input('category', options.category || null)
71
- .input('status', options.status || null)
72
- .input('limit', limit)
73
- .input('offset', offset)
74
- .query(`
75
- SELECT * FROM sf_feedback
76
- WHERE ${where}
77
- ORDER BY ${sortBy} ${sortOrder}
78
- OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY
79
- `);
80
- return {
81
- items: result.recordset.map(mapFeedbackRow),
82
- total,
83
- };
84
- }
85
- export async function updateFeedbackStatus(pool, id, status) {
86
- const existing = await getFeedbackById(pool, id);
87
- if (!existing)
88
- return null;
89
- await pool
90
- .request()
91
- .input('id', id)
92
- .input('status', status)
93
- .query('UPDATE sf_feedback SET status = @status WHERE id = @id');
94
- return { ...existing, status };
95
- }
96
- export async function deleteFeedback(pool, id) {
97
- await pool.request().input('id', id).query('DELETE FROM sf_feedback WHERE id = @id');
98
- return true;
99
- }
100
- // ── Helpers ──
101
- function mapFeedbackRow(row) {
102
- return {
103
- id: row.id,
104
- projectId: row.project_id,
105
- category: row.category || null,
106
- message: row.message,
107
- email: row.email || null,
108
- userAgent: row.user_agent || null,
109
- pageUrl: row.page_url || null,
110
- metadata: parseJsonObject(row.metadata),
111
- status: row.status,
112
- createdAt: row.created_at,
113
- };
114
- }
115
- function parseJsonObject(value) {
116
- if (!value)
117
- return null;
118
- try {
119
- return JSON.parse(value);
120
- }
121
- catch {
122
- return null;
123
- }
124
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * Composable API route handlers for SvelteKit
3
- *
4
- * Usage in consuming app:
5
- * // src/routes/api/status/[...path]/+server.ts
6
- * import { statusServer } from './status';
7
- * const handlers = statusServer.createHandlers();
8
- * export const GET = handlers.GET;
9
- * export const POST = handlers.POST;
10
- * export const PATCH = handlers.PATCH;
11
- * export const DELETE = handlers.DELETE;
12
- */
13
- import type { ConnectionPool } from 'mssql';
14
- type RequestEvent = {
15
- request: Request;
16
- params: Record<string, string>;
17
- url: URL;
18
- };
19
- type Handler = (event: RequestEvent) => Promise<Response>;
20
- export interface RouteHandlers {
21
- GET: Handler;
22
- POST: Handler;
23
- PATCH: Handler;
24
- DELETE: Handler;
25
- }
26
- /**
27
- * Create composable route handlers
28
- *
29
- * Routes:
30
- * GET /systems — list systems for project
31
- * GET /systems/:id — get system details
32
- * GET /systems/:id/history — get status history (90 days)
33
- * GET /incidents — list incidents for project
34
- * GET /incidents/:id — get incident with updates
35
- * GET /feedback — list feedback (admin)
36
- * POST /feedback — submit feedback
37
- * PATCH /systems/:id/status — update system status (admin)
38
- * PATCH /feedback/:id/status — update feedback status (admin)
39
- */
40
- export declare function createRouteHandlers(pool: ConnectionPool): RouteHandlers;
41
- export {};
@@ -1,251 +0,0 @@
1
- /**
2
- * Composable API route handlers for SvelteKit
3
- *
4
- * Usage in consuming app:
5
- * // src/routes/api/status/[...path]/+server.ts
6
- * import { statusServer } from './status';
7
- * const handlers = statusServer.createHandlers();
8
- * export const GET = handlers.GET;
9
- * export const POST = handlers.POST;
10
- * export const PATCH = handlers.PATCH;
11
- * export const DELETE = handlers.DELETE;
12
- */
13
- import { validateApiKey } from './projects.js';
14
- import { createSystem, getSystemsByProject, getSystemById, updateSystem, deleteSystem, updateSystemStatus, getStatusHistory } from './systems.js';
15
- import { createIncident, getIncidentsByProject, getIncidentById, updateIncident, deleteIncident, addIncidentUpdate } from './incidents.js';
16
- import { submitFeedback, getFeedbackByProject } from './feedback.js';
17
- function json(data, status = 200) {
18
- return new Response(JSON.stringify(data), {
19
- status,
20
- headers: { 'Content-Type': 'application/json' },
21
- });
22
- }
23
- function error(message, status = 400) {
24
- return json({ success: false, error: message }, status);
25
- }
26
- /**
27
- * Extract API key from request headers or query params
28
- */
29
- function getApiKey(event) {
30
- const header = event.request.headers.get('x-api-key');
31
- if (header)
32
- return header;
33
- return event.url.searchParams.get('apiKey');
34
- }
35
- /**
36
- * Parse the path segments from the catch-all route param
37
- */
38
- function parsePath(params) {
39
- const path = params.path || '';
40
- return path.split('/').filter(Boolean);
41
- }
42
- /**
43
- * Create composable route handlers
44
- *
45
- * Routes:
46
- * GET /systems — list systems for project
47
- * GET /systems/:id — get system details
48
- * GET /systems/:id/history — get status history (90 days)
49
- * GET /incidents — list incidents for project
50
- * GET /incidents/:id — get incident with updates
51
- * GET /feedback — list feedback (admin)
52
- * POST /feedback — submit feedback
53
- * PATCH /systems/:id/status — update system status (admin)
54
- * PATCH /feedback/:id/status — update feedback status (admin)
55
- */
56
- export function createRouteHandlers(pool) {
57
- async function authenticateProject(event) {
58
- const apiKey = getApiKey(event);
59
- if (!apiKey)
60
- return null;
61
- return validateApiKey(pool, apiKey);
62
- }
63
- const GET = async (event) => {
64
- const project = await authenticateProject(event);
65
- if (!project)
66
- return error('Invalid or missing API key', 401);
67
- const segments = parsePath(event.params);
68
- // GET /systems
69
- if (segments[0] === 'systems' && segments.length === 1) {
70
- const systems = await getSystemsByProject(pool, project.id);
71
- return json({ success: true, data: systems });
72
- }
73
- // GET /systems/:id
74
- if (segments[0] === 'systems' && segments.length === 2) {
75
- const system = await getSystemById(pool, segments[1]);
76
- if (!system || system.projectId !== project.id)
77
- return error('System not found', 404);
78
- return json({ success: true, data: system });
79
- }
80
- // GET /systems/:id/history
81
- if (segments[0] === 'systems' && segments.length === 3 && segments[2] === 'history') {
82
- const days = parseInt(event.url.searchParams.get('days') || '90');
83
- const history = await getStatusHistory(pool, segments[1], days);
84
- return json({ success: true, data: history });
85
- }
86
- // GET /incidents
87
- if (segments[0] === 'incidents' && segments.length === 1) {
88
- const limit = parseInt(event.url.searchParams.get('limit') || '20');
89
- const includeResolved = event.url.searchParams.get('includeResolved') !== 'false';
90
- const incidents = await getIncidentsByProject(pool, project.id, { limit, includeResolved });
91
- return json({ success: true, data: incidents });
92
- }
93
- // GET /incidents/:id
94
- if (segments[0] === 'incidents' && segments.length === 2) {
95
- const incident = await getIncidentById(pool, segments[1]);
96
- if (!incident || incident.projectId !== project.id)
97
- return error('Incident not found', 404);
98
- return json({ success: true, data: incident });
99
- }
100
- // GET /feedback
101
- if (segments[0] === 'feedback' && segments.length === 1) {
102
- const limit = parseInt(event.url.searchParams.get('limit') || '50');
103
- const offset = parseInt(event.url.searchParams.get('offset') || '0');
104
- const category = event.url.searchParams.get('category') || undefined;
105
- const status = event.url.searchParams.get('status');
106
- const result = await getFeedbackByProject(pool, project.id, { limit, offset, category, status });
107
- return json({ success: true, data: result });
108
- }
109
- return error('Not found', 404);
110
- };
111
- const POST = async (event) => {
112
- const project = await authenticateProject(event);
113
- if (!project)
114
- return error('Invalid or missing API key', 401);
115
- const segments = parsePath(event.params);
116
- // POST /systems
117
- if (segments[0] === 'systems' && segments.length === 1) {
118
- const body = await event.request.json();
119
- if (!body.name || typeof body.name !== 'string') {
120
- return error('System name is required');
121
- }
122
- const system = await createSystem(pool, project.id, {
123
- name: body.name,
124
- description: body.description,
125
- displayOrder: body.displayOrder,
126
- });
127
- return json({ success: true, data: system }, 201);
128
- }
129
- // POST /incidents
130
- if (segments[0] === 'incidents' && segments.length === 1) {
131
- const body = await event.request.json();
132
- if (!body.title || typeof body.title !== 'string') {
133
- return error('Incident title is required');
134
- }
135
- const incident = await createIncident(pool, project.id, {
136
- title: body.title,
137
- severity: body.severity,
138
- affectedSystems: body.affectedSystems,
139
- message: body.message,
140
- });
141
- return json({ success: true, data: incident }, 201);
142
- }
143
- // POST /incidents/:id/updates
144
- if (segments[0] === 'incidents' && segments.length === 3 && segments[2] === 'updates') {
145
- const body = await event.request.json();
146
- const validStatuses = ['investigating', 'identified', 'monitoring', 'resolved'];
147
- if (!body.status || !validStatuses.includes(body.status)) {
148
- return error(`Status must be one of: ${validStatuses.join(', ')}`);
149
- }
150
- if (!body.message || typeof body.message !== 'string') {
151
- return error('Message is required');
152
- }
153
- const incident = await getIncidentById(pool, segments[1], false);
154
- if (!incident || incident.projectId !== project.id)
155
- return error('Incident not found', 404);
156
- const update = await addIncidentUpdate(pool, segments[1], {
157
- status: body.status,
158
- message: body.message,
159
- });
160
- return json({ success: true, data: update }, 201);
161
- }
162
- // POST /feedback
163
- if (segments[0] === 'feedback' && segments.length === 1) {
164
- const body = await event.request.json();
165
- if (!body.message || typeof body.message !== 'string' || body.message.trim().length === 0) {
166
- return error('Message is required');
167
- }
168
- if (body.message.length > 5000) {
169
- return error('Message must be under 5000 characters');
170
- }
171
- if (body.email && typeof body.email === 'string' && body.email.length > 255) {
172
- return error('Email must be under 255 characters');
173
- }
174
- const feedback = await submitFeedback(pool, project.id, {
175
- category: body.category,
176
- message: body.message.trim(),
177
- email: body.email,
178
- userAgent: event.request.headers.get('user-agent') || undefined,
179
- pageUrl: body.pageUrl,
180
- metadata: body.metadata,
181
- });
182
- return json({ success: true, data: feedback }, 201);
183
- }
184
- return error('Not found', 404);
185
- };
186
- const PATCH = async (event) => {
187
- const project = await authenticateProject(event);
188
- if (!project)
189
- return error('Invalid or missing API key', 401);
190
- const segments = parsePath(event.params);
191
- const body = await event.request.json();
192
- // PATCH /systems/:id/status
193
- if (segments[0] === 'systems' && segments.length === 3 && segments[2] === 'status') {
194
- const validStatuses = ['operational', 'degraded', 'outage', 'maintenance'];
195
- if (!body.status || !validStatuses.includes(body.status)) {
196
- return error(`Status must be one of: ${validStatuses.join(', ')}`);
197
- }
198
- const system = await getSystemById(pool, segments[1]);
199
- if (!system || system.projectId !== project.id)
200
- return error('System not found', 404);
201
- const updated = await updateSystemStatus(pool, segments[1], body.status);
202
- return json({ success: true, data: updated });
203
- }
204
- // PATCH /feedback/:id/status
205
- if (segments[0] === 'feedback' && segments.length === 3 && segments[2] === 'status') {
206
- const validStatuses = ['new', 'reviewed', 'resolved', 'archived'];
207
- if (!body.status || !validStatuses.includes(body.status)) {
208
- return error(`Status must be one of: ${validStatuses.join(', ')}`);
209
- }
210
- const { updateFeedbackStatus } = await import('./feedback.js');
211
- const updated = await updateFeedbackStatus(pool, segments[1], body.status);
212
- if (!updated)
213
- return error('Feedback not found', 404);
214
- return json({ success: true, data: updated });
215
- }
216
- return error('Not found', 404);
217
- };
218
- const DELETE = async (event) => {
219
- const project = await authenticateProject(event);
220
- if (!project)
221
- return error('Invalid or missing API key', 401);
222
- const segments = parsePath(event.params);
223
- // DELETE /systems/:id
224
- if (segments[0] === 'systems' && segments.length === 2) {
225
- const system = await getSystemById(pool, segments[1]);
226
- if (!system || system.projectId !== project.id)
227
- return error('System not found', 404);
228
- await deleteSystem(pool, segments[1]);
229
- return json({ success: true });
230
- }
231
- // DELETE /incidents/:id
232
- if (segments[0] === 'incidents' && segments.length === 2) {
233
- const incident = await getIncidentById(pool, segments[1], false);
234
- if (!incident || incident.projectId !== project.id)
235
- return error('Incident not found', 404);
236
- await deleteIncident(pool, segments[1]);
237
- return json({ success: true });
238
- }
239
- // DELETE /feedback/:id
240
- if (segments[0] === 'feedback' && segments.length === 2) {
241
- const { getFeedbackById, deleteFeedback } = await import('./feedback.js');
242
- const existing = await getFeedbackById(pool, segments[1]);
243
- if (!existing || existing.projectId !== project.id)
244
- return error('Feedback not found', 404);
245
- await deleteFeedback(pool, segments[1]);
246
- return json({ success: true });
247
- }
248
- return error('Not found', 404);
249
- };
250
- return { GET, POST, PATCH, DELETE };
251
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Incidents CRUD + updates
3
- */
4
- import type { ConnectionPool } from 'mssql';
5
- import type { Incident, IncidentUpdate, IncidentStatus, IncidentSeverity } from './types.js';
6
- export declare function createIncident(pool: ConnectionPool, projectId: string, data: {
7
- title: string;
8
- severity?: IncidentSeverity;
9
- affectedSystems?: string[];
10
- message?: string;
11
- }): Promise<Incident>;
12
- export declare function getIncidentById(pool: ConnectionPool, id: string, includeUpdates?: boolean): Promise<Incident | null>;
13
- export declare function getIncidentsByProject(pool: ConnectionPool, projectId: string, options?: {
14
- limit?: number;
15
- includeResolved?: boolean;
16
- }): Promise<Incident[]>;
17
- export declare function updateIncident(pool: ConnectionPool, id: string, data: {
18
- title?: string;
19
- severity?: IncidentSeverity;
20
- affectedSystems?: string[];
21
- }): Promise<Incident | null>;
22
- export declare function deleteIncident(pool: ConnectionPool, id: string): Promise<boolean>;
23
- export declare function addIncidentUpdate(pool: ConnectionPool, incidentId: string, data: {
24
- status: IncidentStatus;
25
- message: string;
26
- }): Promise<IncidentUpdate>;
27
- export declare function getIncidentUpdates(pool: ConnectionPool, incidentId: string): Promise<IncidentUpdate[]>;
@@ -1,180 +0,0 @@
1
- /**
2
- * Incidents CRUD + updates
3
- */
4
- import { nanoid } from 'nanoid';
5
- // ── CRUD ──
6
- export async function createIncident(pool, projectId, data) {
7
- const id = nanoid(16);
8
- const now = Date.now();
9
- const severity = data.severity ?? 'minor';
10
- await pool
11
- .request()
12
- .input('id', id)
13
- .input('projectId', projectId)
14
- .input('title', data.title)
15
- .input('severity', severity)
16
- .input('affectedSystems', JSON.stringify(data.affectedSystems || []))
17
- .input('createdAt', now)
18
- .input('updatedAt', now)
19
- .query(`
20
- INSERT INTO sf_incidents (id, project_id, title, status, severity, affected_systems, created_at, updated_at)
21
- VALUES (@id, @projectId, @title, 'investigating', @severity, @affectedSystems, @createdAt, @updatedAt)
22
- `);
23
- // Create initial update if message provided
24
- const updates = [];
25
- if (data.message) {
26
- const update = await addIncidentUpdate(pool, id, {
27
- status: 'investigating',
28
- message: data.message,
29
- });
30
- updates.push(update);
31
- }
32
- return {
33
- id,
34
- projectId,
35
- title: data.title,
36
- status: 'investigating',
37
- severity,
38
- affectedSystems: data.affectedSystems || [],
39
- createdAt: now,
40
- updatedAt: now,
41
- resolvedAt: null,
42
- updates,
43
- };
44
- }
45
- export async function getIncidentById(pool, id, includeUpdates = true) {
46
- const result = await pool
47
- .request()
48
- .input('id', id)
49
- .query('SELECT * FROM sf_incidents WHERE id = @id');
50
- if (result.recordset.length === 0)
51
- return null;
52
- const incident = mapIncidentRow(result.recordset[0]);
53
- if (includeUpdates) {
54
- incident.updates = await getIncidentUpdates(pool, id);
55
- }
56
- return incident;
57
- }
58
- export async function getIncidentsByProject(pool, projectId, options = {}) {
59
- const limit = options.limit ?? 50;
60
- const includeResolved = options.includeResolved ?? true;
61
- let query = 'SELECT TOP (@limit) * FROM sf_incidents WHERE project_id = @projectId';
62
- if (!includeResolved) {
63
- query += " AND status != 'resolved'";
64
- }
65
- query += ' ORDER BY created_at DESC';
66
- const result = await pool
67
- .request()
68
- .input('projectId', projectId)
69
- .input('limit', limit)
70
- .query(query);
71
- const incidents = result.recordset.map(mapIncidentRow);
72
- // Load updates for each incident
73
- for (const incident of incidents) {
74
- incident.updates = await getIncidentUpdates(pool, incident.id);
75
- }
76
- return incidents;
77
- }
78
- export async function updateIncident(pool, id, data) {
79
- const existing = await getIncidentById(pool, id, false);
80
- if (!existing)
81
- return null;
82
- const now = Date.now();
83
- await pool
84
- .request()
85
- .input('id', id)
86
- .input('title', data.title ?? existing.title)
87
- .input('severity', data.severity ?? existing.severity)
88
- .input('affectedSystems', JSON.stringify(data.affectedSystems ?? existing.affectedSystems))
89
- .input('updatedAt', now)
90
- .query(`
91
- UPDATE sf_incidents
92
- SET title = @title, severity = @severity, affected_systems = @affectedSystems, updated_at = @updatedAt
93
- WHERE id = @id
94
- `);
95
- return {
96
- ...existing,
97
- title: data.title ?? existing.title,
98
- severity: data.severity ?? existing.severity,
99
- affectedSystems: data.affectedSystems ?? existing.affectedSystems,
100
- updatedAt: now,
101
- };
102
- }
103
- export async function deleteIncident(pool, id) {
104
- await pool.request().input('id', id).query(`
105
- DELETE FROM sf_incident_updates WHERE incident_id = @id;
106
- DELETE FROM sf_incidents WHERE id = @id;
107
- `);
108
- return true;
109
- }
110
- // ── Incident Updates ──
111
- export async function addIncidentUpdate(pool, incidentId, data) {
112
- const id = nanoid(16);
113
- const now = Date.now();
114
- await pool
115
- .request()
116
- .input('id', id)
117
- .input('incidentId', incidentId)
118
- .input('status', data.status)
119
- .input('message', data.message)
120
- .input('createdAt', now)
121
- .query(`
122
- INSERT INTO sf_incident_updates (id, incident_id, status, message, created_at)
123
- VALUES (@id, @incidentId, @status, @message, @createdAt)
124
- `);
125
- // Update incident status + resolved_at
126
- const resolvedAt = data.status === 'resolved' ? now : null;
127
- await pool
128
- .request()
129
- .input('incidentId', incidentId)
130
- .input('status', data.status)
131
- .input('updatedAt', now)
132
- .input('resolvedAt', resolvedAt)
133
- .query(`
134
- UPDATE sf_incidents
135
- SET status = @status, updated_at = @updatedAt, resolved_at = COALESCE(@resolvedAt, resolved_at)
136
- WHERE id = @incidentId
137
- `);
138
- return { id, incidentId, status: data.status, message: data.message, createdAt: now };
139
- }
140
- export async function getIncidentUpdates(pool, incidentId) {
141
- const result = await pool
142
- .request()
143
- .input('incidentId', incidentId)
144
- .query('SELECT * FROM sf_incident_updates WHERE incident_id = @incidentId ORDER BY created_at DESC');
145
- return result.recordset.map(mapUpdateRow);
146
- }
147
- // ── Helpers ──
148
- function mapIncidentRow(row) {
149
- return {
150
- id: row.id,
151
- projectId: row.project_id,
152
- title: row.title,
153
- status: row.status,
154
- severity: row.severity,
155
- affectedSystems: parseJsonArray(row.affected_systems),
156
- createdAt: row.created_at,
157
- updatedAt: row.updated_at,
158
- resolvedAt: row.resolved_at || null,
159
- };
160
- }
161
- function mapUpdateRow(row) {
162
- return {
163
- id: row.id,
164
- incidentId: row.incident_id,
165
- status: row.status,
166
- message: row.message,
167
- createdAt: row.created_at,
168
- };
169
- }
170
- function parseJsonArray(value) {
171
- if (!value)
172
- return [];
173
- try {
174
- const parsed = JSON.parse(value);
175
- return Array.isArray(parsed) ? parsed : [];
176
- }
177
- catch {
178
- return [];
179
- }
180
- }