@alliance-droid/status-feedback-system 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/README.md +95 -0
- package/dist/components/FeedbackButton.svelte +72 -0
- package/dist/components/FeedbackButton.svelte.d.ts +10 -0
- package/dist/components/FeedbackForm.svelte +151 -0
- package/dist/components/FeedbackForm.svelte.d.ts +11 -0
- package/dist/components/StatusBanner.svelte +41 -0
- package/dist/components/StatusBanner.svelte.d.ts +8 -0
- package/dist/components/StatusPage.svelte +156 -0
- package/dist/components/StatusPage.svelte.d.ts +12 -0
- package/dist/components/admin/AdminDashboard.svelte +274 -0
- package/dist/components/admin/AdminDashboard.svelte.d.ts +9 -0
- package/dist/components/admin/ApiKeyManager.svelte +68 -0
- package/dist/components/admin/ApiKeyManager.svelte.d.ts +7 -0
- package/dist/components/admin/FeedbackList.svelte +160 -0
- package/dist/components/admin/FeedbackList.svelte.d.ts +17 -0
- package/dist/components/admin/IncidentForm.svelte +135 -0
- package/dist/components/admin/IncidentForm.svelte.d.ts +14 -0
- package/dist/components/admin/IncidentManager.svelte +153 -0
- package/dist/components/admin/IncidentManager.svelte.d.ts +13 -0
- package/dist/components/admin/ProjectForm.svelte +106 -0
- package/dist/components/admin/ProjectForm.svelte.d.ts +13 -0
- package/dist/components/admin/ProjectList.svelte +57 -0
- package/dist/components/admin/ProjectList.svelte.d.ts +10 -0
- package/dist/components/admin/StatusControl.svelte +62 -0
- package/dist/components/admin/StatusControl.svelte.d.ts +8 -0
- package/dist/components/admin/SystemManager.svelte +119 -0
- package/dist/components/admin/SystemManager.svelte.d.ts +13 -0
- package/dist/components/admin/index.d.ts +9 -0
- package/dist/components/admin/index.js +9 -0
- package/dist/components/shared/IncidentTimeline.svelte +55 -0
- package/dist/components/shared/IncidentTimeline.svelte.d.ts +7 -0
- package/dist/components/shared/StatusIndicator.svelte +50 -0
- package/dist/components/shared/StatusIndicator.svelte.d.ts +9 -0
- package/dist/components/shared/SystemCard.svelte +30 -0
- package/dist/components/shared/SystemCard.svelte.d.ts +9 -0
- package/dist/components/shared/UptimeBar.svelte +85 -0
- package/dist/components/shared/UptimeBar.svelte.d.ts +8 -0
- package/dist/components/shared/index.d.ts +4 -0
- package/dist/components/shared/index.js +4 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +24 -0
- package/dist/server/db.d.ts +17 -0
- package/dist/server/db.js +68 -0
- package/dist/server/feedback.d.ts +27 -0
- package/dist/server/feedback.js +124 -0
- package/dist/server/handlers.d.ts +41 -0
- package/dist/server/handlers.js +251 -0
- package/dist/server/incidents.d.ts +27 -0
- package/dist/server/incidents.js +180 -0
- package/dist/server/index.d.ts +101 -0
- package/dist/server/index.js +71 -0
- package/dist/server/projects.d.ts +45 -0
- package/dist/server/projects.js +156 -0
- package/dist/server/schema.d.ts +9 -0
- package/dist/server/schema.js +94 -0
- package/dist/server/systems.d.ts +20 -0
- package/dist/server/systems.js +146 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +4 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +4 -0
- package/package.json +71 -0
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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[]>;
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server module for @alliance-droid/status-feedback-system
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { createStatusServer } from '@alliance-droid/status-feedback-system/server';
|
|
6
|
+
*
|
|
7
|
+
* const server = createStatusServer({
|
|
8
|
+
* database: { connectionString: env.MSSQL_CONNECTION_STRING }
|
|
9
|
+
* });
|
|
10
|
+
* await server.init();
|
|
11
|
+
*/
|
|
12
|
+
import { type RouteHandlers } from './handlers.js';
|
|
13
|
+
import type { StatusServerConfig, Project, System, SystemStatus, Incident, IncidentStatus, IncidentSeverity, IncidentUpdate, Feedback, FeedbackStatus, StatusHistoryEntry } from './types.js';
|
|
14
|
+
export interface StatusServer {
|
|
15
|
+
/** Initialize database connection and run migrations */
|
|
16
|
+
init(): Promise<void>;
|
|
17
|
+
/** Close the database connection */
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
createProject(data: {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
allowedOrigins?: string[];
|
|
23
|
+
}): Promise<Project>;
|
|
24
|
+
getProject(id: string): Promise<Project | null>;
|
|
25
|
+
getProjectByApiKey(apiKey: string): Promise<Project | null>;
|
|
26
|
+
listProjects(): Promise<Project[]>;
|
|
27
|
+
updateProject(id: string, data: {
|
|
28
|
+
name?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
allowedOrigins?: string[];
|
|
31
|
+
}): Promise<Project | null>;
|
|
32
|
+
deleteProject(id: string): Promise<boolean>;
|
|
33
|
+
regenerateApiKey(id: string): Promise<string | null>;
|
|
34
|
+
validateApiKey(apiKey: string): Promise<Project | null>;
|
|
35
|
+
createSystem(projectId: string, data: {
|
|
36
|
+
name: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
displayOrder?: number;
|
|
39
|
+
}): Promise<System>;
|
|
40
|
+
getSystem(id: string): Promise<System | null>;
|
|
41
|
+
getSystems(projectId: string): Promise<System[]>;
|
|
42
|
+
updateSystem(id: string, data: {
|
|
43
|
+
name?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
displayOrder?: number;
|
|
46
|
+
}): Promise<System | null>;
|
|
47
|
+
deleteSystem(id: string): Promise<boolean>;
|
|
48
|
+
updateSystemStatus(id: string, status: SystemStatus): Promise<System | null>;
|
|
49
|
+
getStatusHistory(systemId: string, days?: number): Promise<StatusHistoryEntry[]>;
|
|
50
|
+
createIncident(projectId: string, data: {
|
|
51
|
+
title: string;
|
|
52
|
+
severity?: IncidentSeverity;
|
|
53
|
+
affectedSystems?: string[];
|
|
54
|
+
message?: string;
|
|
55
|
+
}): Promise<Incident>;
|
|
56
|
+
getIncident(id: string): Promise<Incident | null>;
|
|
57
|
+
getIncidents(projectId: string, options?: {
|
|
58
|
+
limit?: number;
|
|
59
|
+
includeResolved?: boolean;
|
|
60
|
+
}): Promise<Incident[]>;
|
|
61
|
+
updateIncident(id: string, data: {
|
|
62
|
+
title?: string;
|
|
63
|
+
severity?: IncidentSeverity;
|
|
64
|
+
affectedSystems?: string[];
|
|
65
|
+
}): Promise<Incident | null>;
|
|
66
|
+
deleteIncident(id: string): Promise<boolean>;
|
|
67
|
+
addIncidentUpdate(incidentId: string, data: {
|
|
68
|
+
status: IncidentStatus;
|
|
69
|
+
message: string;
|
|
70
|
+
}): Promise<IncidentUpdate>;
|
|
71
|
+
getIncidentUpdates(incidentId: string): Promise<IncidentUpdate[]>;
|
|
72
|
+
submitFeedback(projectId: string, data: {
|
|
73
|
+
category?: string;
|
|
74
|
+
message: string;
|
|
75
|
+
email?: string;
|
|
76
|
+
userAgent?: string;
|
|
77
|
+
pageUrl?: string;
|
|
78
|
+
metadata?: Record<string, unknown>;
|
|
79
|
+
}): Promise<Feedback>;
|
|
80
|
+
getFeedback(id: string): Promise<Feedback | null>;
|
|
81
|
+
getFeedbackByProject(projectId: string, options?: {
|
|
82
|
+
limit?: number;
|
|
83
|
+
offset?: number;
|
|
84
|
+
category?: string;
|
|
85
|
+
status?: FeedbackStatus;
|
|
86
|
+
sortBy?: 'created_at' | 'status';
|
|
87
|
+
sortOrder?: 'ASC' | 'DESC';
|
|
88
|
+
}): Promise<{
|
|
89
|
+
items: Feedback[];
|
|
90
|
+
total: number;
|
|
91
|
+
}>;
|
|
92
|
+
updateFeedbackStatus(id: string, status: FeedbackStatus): Promise<Feedback | null>;
|
|
93
|
+
deleteFeedback(id: string): Promise<boolean>;
|
|
94
|
+
createHandlers(): RouteHandlers;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create a status server instance
|
|
98
|
+
*/
|
|
99
|
+
export declare function createStatusServer(config: StatusServerConfig): StatusServer;
|
|
100
|
+
export type { Project, System, Incident, IncidentUpdate, Feedback, StatusHistoryEntry, SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus, StatusServerConfig, ApiResponse, } from './types.js';
|
|
101
|
+
export type { RouteHandlers } from './handlers.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server module for @alliance-droid/status-feedback-system
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { createStatusServer } from '@alliance-droid/status-feedback-system/server';
|
|
6
|
+
*
|
|
7
|
+
* const server = createStatusServer({
|
|
8
|
+
* database: { connectionString: env.MSSQL_CONNECTION_STRING }
|
|
9
|
+
* });
|
|
10
|
+
* await server.init();
|
|
11
|
+
*/
|
|
12
|
+
import { initDatabase, closeDatabase } from './db.js';
|
|
13
|
+
import { createProject, getProjectById, getProjectByApiKey, listProjects, updateProject, deleteProject, regenerateApiKey, validateApiKey, } from './projects.js';
|
|
14
|
+
import { createSystem, getSystemById, getSystemsByProject, updateSystem, deleteSystem, updateSystemStatus, getStatusHistory, } from './systems.js';
|
|
15
|
+
import { createIncident, getIncidentById, getIncidentsByProject, updateIncident, deleteIncident, addIncidentUpdate, getIncidentUpdates, } from './incidents.js';
|
|
16
|
+
import { submitFeedback, getFeedbackById, getFeedbackByProject, updateFeedbackStatus, deleteFeedback, } from './feedback.js';
|
|
17
|
+
import { createRouteHandlers } from './handlers.js';
|
|
18
|
+
/**
|
|
19
|
+
* Create a status server instance
|
|
20
|
+
*/
|
|
21
|
+
export function createStatusServer(config) {
|
|
22
|
+
let pool = null;
|
|
23
|
+
function getPool() {
|
|
24
|
+
if (!pool) {
|
|
25
|
+
throw new Error('StatusServer not initialized. Call init() first.');
|
|
26
|
+
}
|
|
27
|
+
return pool;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async init() {
|
|
31
|
+
pool = await initDatabase(config.database.connectionString);
|
|
32
|
+
},
|
|
33
|
+
async close() {
|
|
34
|
+
await closeDatabase();
|
|
35
|
+
pool = null;
|
|
36
|
+
},
|
|
37
|
+
// ── Project Management ──
|
|
38
|
+
createProject: (data) => createProject(getPool(), data),
|
|
39
|
+
getProject: (id) => getProjectById(getPool(), id),
|
|
40
|
+
getProjectByApiKey: (apiKey) => getProjectByApiKey(getPool(), apiKey),
|
|
41
|
+
listProjects: () => listProjects(getPool()),
|
|
42
|
+
updateProject: (id, data) => updateProject(getPool(), id, data),
|
|
43
|
+
deleteProject: (id) => deleteProject(getPool(), id),
|
|
44
|
+
regenerateApiKey: (id) => regenerateApiKey(getPool(), id),
|
|
45
|
+
validateApiKey: (apiKey) => validateApiKey(getPool(), apiKey),
|
|
46
|
+
// ── System Management ──
|
|
47
|
+
createSystem: (projectId, data) => createSystem(getPool(), projectId, data),
|
|
48
|
+
getSystem: (id) => getSystemById(getPool(), id),
|
|
49
|
+
getSystems: (projectId) => getSystemsByProject(getPool(), projectId),
|
|
50
|
+
updateSystem: (id, data) => updateSystem(getPool(), id, data),
|
|
51
|
+
deleteSystem: (id) => deleteSystem(getPool(), id),
|
|
52
|
+
updateSystemStatus: (id, status) => updateSystemStatus(getPool(), id, status),
|
|
53
|
+
getStatusHistory: (systemId, days) => getStatusHistory(getPool(), systemId, days),
|
|
54
|
+
// ── Incident Management ──
|
|
55
|
+
createIncident: (projectId, data) => createIncident(getPool(), projectId, data),
|
|
56
|
+
getIncident: (id) => getIncidentById(getPool(), id),
|
|
57
|
+
getIncidents: (projectId, options) => getIncidentsByProject(getPool(), projectId, options),
|
|
58
|
+
updateIncident: (id, data) => updateIncident(getPool(), id, data),
|
|
59
|
+
deleteIncident: (id) => deleteIncident(getPool(), id),
|
|
60
|
+
addIncidentUpdate: (incidentId, data) => addIncidentUpdate(getPool(), incidentId, data),
|
|
61
|
+
getIncidentUpdates: (incidentId) => getIncidentUpdates(getPool(), incidentId),
|
|
62
|
+
// ── Feedback Management ──
|
|
63
|
+
submitFeedback: (projectId, data) => submitFeedback(getPool(), projectId, data),
|
|
64
|
+
getFeedback: (id) => getFeedbackById(getPool(), id),
|
|
65
|
+
getFeedbackByProject: (projectId, options) => getFeedbackByProject(getPool(), projectId, options),
|
|
66
|
+
updateFeedbackStatus: (id, status) => updateFeedbackStatus(getPool(), id, status),
|
|
67
|
+
deleteFeedback: (id) => deleteFeedback(getPool(), id),
|
|
68
|
+
// ── Route Handlers ──
|
|
69
|
+
createHandlers: () => createRouteHandlers(getPool()),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project CRUD + API key management
|
|
3
|
+
*/
|
|
4
|
+
import type { ConnectionPool } from 'mssql';
|
|
5
|
+
import type { Project } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Create a new project
|
|
8
|
+
*/
|
|
9
|
+
export declare function createProject(pool: ConnectionPool, data: {
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
allowedOrigins?: string[];
|
|
13
|
+
}): Promise<Project>;
|
|
14
|
+
/**
|
|
15
|
+
* Get a project by API key
|
|
16
|
+
*/
|
|
17
|
+
export declare function getProjectByApiKey(pool: ConnectionPool, apiKey: string): Promise<Project | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Get a project by ID
|
|
20
|
+
*/
|
|
21
|
+
export declare function getProjectById(pool: ConnectionPool, id: string): Promise<Project | null>;
|
|
22
|
+
/**
|
|
23
|
+
* List all projects
|
|
24
|
+
*/
|
|
25
|
+
export declare function listProjects(pool: ConnectionPool): Promise<Project[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Update a project
|
|
28
|
+
*/
|
|
29
|
+
export declare function updateProject(pool: ConnectionPool, id: string, data: {
|
|
30
|
+
name?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
allowedOrigins?: string[];
|
|
33
|
+
}): Promise<Project | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Delete a project and all related data
|
|
36
|
+
*/
|
|
37
|
+
export declare function deleteProject(pool: ConnectionPool, id: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Regenerate a project's API key
|
|
40
|
+
*/
|
|
41
|
+
export declare function regenerateApiKey(pool: ConnectionPool, id: string): Promise<string | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Validate an API key and return the project
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateApiKey(pool: ConnectionPool, apiKey: string): Promise<Project | null>;
|