@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.
- package/dist/api.d.ts +94 -0
- package/dist/api.js +100 -0
- package/dist/components/FeedbackBoard.svelte +842 -0
- package/dist/components/FeedbackBoard.svelte.d.ts +19 -0
- package/dist/components/FeedbackButton.svelte +3 -3
- package/dist/components/FeedbackButton.svelte.d.ts +1 -1
- package/dist/components/FeedbackForm.svelte +3 -3
- package/dist/components/FeedbackForm.svelte.d.ts +1 -1
- package/dist/components/StatusBanner.svelte +36 -3
- package/dist/components/StatusBanner.svelte.d.ts +7 -1
- package/dist/components/StatusPage.svelte +150 -83
- package/dist/components/StatusPage.svelte.d.ts +10 -3
- package/dist/index.d.ts +7 -9
- package/dist/index.js +6 -8
- package/dist/types.d.ts +13 -6
- package/package.json +2 -13
- package/dist/components/admin/AdminDashboard.svelte +0 -274
- package/dist/components/admin/AdminDashboard.svelte.d.ts +0 -9
- package/dist/components/admin/ApiKeyManager.svelte +0 -68
- package/dist/components/admin/ApiKeyManager.svelte.d.ts +0 -7
- package/dist/components/admin/FeedbackList.svelte +0 -160
- package/dist/components/admin/FeedbackList.svelte.d.ts +0 -17
- package/dist/components/admin/IncidentForm.svelte +0 -135
- package/dist/components/admin/IncidentForm.svelte.d.ts +0 -14
- package/dist/components/admin/IncidentManager.svelte +0 -153
- package/dist/components/admin/IncidentManager.svelte.d.ts +0 -13
- package/dist/components/admin/ProjectForm.svelte +0 -106
- package/dist/components/admin/ProjectForm.svelte.d.ts +0 -13
- package/dist/components/admin/ProjectList.svelte +0 -57
- package/dist/components/admin/ProjectList.svelte.d.ts +0 -10
- package/dist/components/admin/StatusControl.svelte +0 -62
- package/dist/components/admin/StatusControl.svelte.d.ts +0 -8
- package/dist/components/admin/SystemManager.svelte +0 -119
- package/dist/components/admin/SystemManager.svelte.d.ts +0 -13
- package/dist/components/admin/index.d.ts +0 -9
- package/dist/components/admin/index.js +0 -9
- package/dist/server/db.d.ts +0 -17
- package/dist/server/db.js +0 -68
- package/dist/server/feedback.d.ts +0 -27
- package/dist/server/feedback.js +0 -124
- package/dist/server/handlers.d.ts +0 -41
- package/dist/server/handlers.js +0 -251
- package/dist/server/incidents.d.ts +0 -27
- package/dist/server/incidents.js +0 -180
- package/dist/server/index.d.ts +0 -101
- package/dist/server/index.js +0 -71
- package/dist/server/projects.d.ts +0 -45
- package/dist/server/projects.js +0 -156
- package/dist/server/schema.d.ts +0 -9
- package/dist/server/schema.js +0 -94
- package/dist/server/systems.d.ts +0 -20
- package/dist/server/systems.js +0 -146
- package/dist/server/types.d.ts +0 -4
- 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>;
|
package/dist/server/feedback.js
DELETED
|
@@ -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 {};
|
package/dist/server/handlers.js
DELETED
|
@@ -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[]>;
|
package/dist/server/incidents.js
DELETED
|
@@ -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
|
-
}
|