@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.
Files changed (62) hide show
  1. package/README.md +95 -0
  2. package/dist/components/FeedbackButton.svelte +72 -0
  3. package/dist/components/FeedbackButton.svelte.d.ts +10 -0
  4. package/dist/components/FeedbackForm.svelte +151 -0
  5. package/dist/components/FeedbackForm.svelte.d.ts +11 -0
  6. package/dist/components/StatusBanner.svelte +41 -0
  7. package/dist/components/StatusBanner.svelte.d.ts +8 -0
  8. package/dist/components/StatusPage.svelte +156 -0
  9. package/dist/components/StatusPage.svelte.d.ts +12 -0
  10. package/dist/components/admin/AdminDashboard.svelte +274 -0
  11. package/dist/components/admin/AdminDashboard.svelte.d.ts +9 -0
  12. package/dist/components/admin/ApiKeyManager.svelte +68 -0
  13. package/dist/components/admin/ApiKeyManager.svelte.d.ts +7 -0
  14. package/dist/components/admin/FeedbackList.svelte +160 -0
  15. package/dist/components/admin/FeedbackList.svelte.d.ts +17 -0
  16. package/dist/components/admin/IncidentForm.svelte +135 -0
  17. package/dist/components/admin/IncidentForm.svelte.d.ts +14 -0
  18. package/dist/components/admin/IncidentManager.svelte +153 -0
  19. package/dist/components/admin/IncidentManager.svelte.d.ts +13 -0
  20. package/dist/components/admin/ProjectForm.svelte +106 -0
  21. package/dist/components/admin/ProjectForm.svelte.d.ts +13 -0
  22. package/dist/components/admin/ProjectList.svelte +57 -0
  23. package/dist/components/admin/ProjectList.svelte.d.ts +10 -0
  24. package/dist/components/admin/StatusControl.svelte +62 -0
  25. package/dist/components/admin/StatusControl.svelte.d.ts +8 -0
  26. package/dist/components/admin/SystemManager.svelte +119 -0
  27. package/dist/components/admin/SystemManager.svelte.d.ts +13 -0
  28. package/dist/components/admin/index.d.ts +9 -0
  29. package/dist/components/admin/index.js +9 -0
  30. package/dist/components/shared/IncidentTimeline.svelte +55 -0
  31. package/dist/components/shared/IncidentTimeline.svelte.d.ts +7 -0
  32. package/dist/components/shared/StatusIndicator.svelte +50 -0
  33. package/dist/components/shared/StatusIndicator.svelte.d.ts +9 -0
  34. package/dist/components/shared/SystemCard.svelte +30 -0
  35. package/dist/components/shared/SystemCard.svelte.d.ts +9 -0
  36. package/dist/components/shared/UptimeBar.svelte +85 -0
  37. package/dist/components/shared/UptimeBar.svelte.d.ts +8 -0
  38. package/dist/components/shared/index.d.ts +4 -0
  39. package/dist/components/shared/index.js +4 -0
  40. package/dist/index.d.ts +23 -0
  41. package/dist/index.js +24 -0
  42. package/dist/server/db.d.ts +17 -0
  43. package/dist/server/db.js +68 -0
  44. package/dist/server/feedback.d.ts +27 -0
  45. package/dist/server/feedback.js +124 -0
  46. package/dist/server/handlers.d.ts +41 -0
  47. package/dist/server/handlers.js +251 -0
  48. package/dist/server/incidents.d.ts +27 -0
  49. package/dist/server/incidents.js +180 -0
  50. package/dist/server/index.d.ts +101 -0
  51. package/dist/server/index.js +71 -0
  52. package/dist/server/projects.d.ts +45 -0
  53. package/dist/server/projects.js +156 -0
  54. package/dist/server/schema.d.ts +9 -0
  55. package/dist/server/schema.js +94 -0
  56. package/dist/server/systems.d.ts +20 -0
  57. package/dist/server/systems.js +146 -0
  58. package/dist/server/types.d.ts +4 -0
  59. package/dist/server/types.js +4 -0
  60. package/dist/types.d.ts +73 -0
  61. package/dist/types.js +4 -0
  62. 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>;