@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project CRUD + API key management
|
|
3
|
+
*/
|
|
4
|
+
import { nanoid } from 'nanoid';
|
|
5
|
+
/**
|
|
6
|
+
* Generate a prefixed API key
|
|
7
|
+
*/
|
|
8
|
+
function generateApiKey() {
|
|
9
|
+
return `sf_${nanoid(32)}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a new project
|
|
13
|
+
*/
|
|
14
|
+
export async function createProject(pool, data) {
|
|
15
|
+
const id = nanoid(16);
|
|
16
|
+
const apiKey = generateApiKey();
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
await pool
|
|
19
|
+
.request()
|
|
20
|
+
.input('id', id)
|
|
21
|
+
.input('name', data.name)
|
|
22
|
+
.input('apiKey', apiKey)
|
|
23
|
+
.input('description', data.description || null)
|
|
24
|
+
.input('allowedOrigins', JSON.stringify(data.allowedOrigins || []))
|
|
25
|
+
.input('createdAt', now)
|
|
26
|
+
.input('updatedAt', now)
|
|
27
|
+
.query(`
|
|
28
|
+
INSERT INTO sf_projects (id, name, api_key, description, allowed_origins, created_at, updated_at)
|
|
29
|
+
VALUES (@id, @name, @apiKey, @description, @allowedOrigins, @createdAt, @updatedAt)
|
|
30
|
+
`);
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
name: data.name,
|
|
34
|
+
apiKey,
|
|
35
|
+
description: data.description || null,
|
|
36
|
+
allowedOrigins: data.allowedOrigins || [],
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get a project by API key
|
|
43
|
+
*/
|
|
44
|
+
export async function getProjectByApiKey(pool, apiKey) {
|
|
45
|
+
const result = await pool
|
|
46
|
+
.request()
|
|
47
|
+
.input('apiKey', apiKey)
|
|
48
|
+
.query('SELECT * FROM sf_projects WHERE api_key = @apiKey');
|
|
49
|
+
if (result.recordset.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return mapRow(result.recordset[0]);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get a project by ID
|
|
55
|
+
*/
|
|
56
|
+
export async function getProjectById(pool, id) {
|
|
57
|
+
const result = await pool
|
|
58
|
+
.request()
|
|
59
|
+
.input('id', id)
|
|
60
|
+
.query('SELECT * FROM sf_projects WHERE id = @id');
|
|
61
|
+
if (result.recordset.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
return mapRow(result.recordset[0]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* List all projects
|
|
67
|
+
*/
|
|
68
|
+
export async function listProjects(pool) {
|
|
69
|
+
const result = await pool.request().query('SELECT * FROM sf_projects ORDER BY created_at DESC');
|
|
70
|
+
return result.recordset.map(mapRow);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Update a project
|
|
74
|
+
*/
|
|
75
|
+
export async function updateProject(pool, id, data) {
|
|
76
|
+
const existing = await getProjectById(pool, id);
|
|
77
|
+
if (!existing)
|
|
78
|
+
return null;
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
await pool
|
|
81
|
+
.request()
|
|
82
|
+
.input('id', id)
|
|
83
|
+
.input('name', data.name ?? existing.name)
|
|
84
|
+
.input('description', data.description ?? existing.description)
|
|
85
|
+
.input('allowedOrigins', JSON.stringify(data.allowedOrigins ?? existing.allowedOrigins))
|
|
86
|
+
.input('updatedAt', now)
|
|
87
|
+
.query(`
|
|
88
|
+
UPDATE sf_projects
|
|
89
|
+
SET name = @name, description = @description, allowed_origins = @allowedOrigins, updated_at = @updatedAt
|
|
90
|
+
WHERE id = @id
|
|
91
|
+
`);
|
|
92
|
+
return { ...existing, ...data, updatedAt: now };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Delete a project and all related data
|
|
96
|
+
*/
|
|
97
|
+
export async function deleteProject(pool, id) {
|
|
98
|
+
// Delete in dependency order
|
|
99
|
+
await pool.request().input('id', id).query(`
|
|
100
|
+
DELETE FROM sf_status_history WHERE system_id IN (SELECT id FROM sf_systems WHERE project_id = @id);
|
|
101
|
+
DELETE FROM sf_incident_updates WHERE incident_id IN (SELECT id FROM sf_incidents WHERE project_id = @id);
|
|
102
|
+
DELETE FROM sf_incidents WHERE project_id = @id;
|
|
103
|
+
DELETE FROM sf_systems WHERE project_id = @id;
|
|
104
|
+
DELETE FROM sf_feedback WHERE project_id = @id;
|
|
105
|
+
DELETE FROM sf_projects WHERE id = @id;
|
|
106
|
+
`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Regenerate a project's API key
|
|
111
|
+
*/
|
|
112
|
+
export async function regenerateApiKey(pool, id) {
|
|
113
|
+
const existing = await getProjectById(pool, id);
|
|
114
|
+
if (!existing)
|
|
115
|
+
return null;
|
|
116
|
+
const newKey = generateApiKey();
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
await pool
|
|
119
|
+
.request()
|
|
120
|
+
.input('id', id)
|
|
121
|
+
.input('apiKey', newKey)
|
|
122
|
+
.input('updatedAt', now)
|
|
123
|
+
.query('UPDATE sf_projects SET api_key = @apiKey, updated_at = @updatedAt WHERE id = @id');
|
|
124
|
+
return newKey;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Validate an API key and return the project
|
|
128
|
+
*/
|
|
129
|
+
export async function validateApiKey(pool, apiKey) {
|
|
130
|
+
if (!apiKey || !apiKey.startsWith('sf_'))
|
|
131
|
+
return null;
|
|
132
|
+
return getProjectByApiKey(pool, apiKey);
|
|
133
|
+
}
|
|
134
|
+
// ── Helpers ──
|
|
135
|
+
function mapRow(row) {
|
|
136
|
+
return {
|
|
137
|
+
id: row.id,
|
|
138
|
+
name: row.name,
|
|
139
|
+
apiKey: row.api_key,
|
|
140
|
+
description: row.description || null,
|
|
141
|
+
allowedOrigins: parseJsonArray(row.allowed_origins),
|
|
142
|
+
createdAt: row.created_at,
|
|
143
|
+
updatedAt: row.updated_at,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function parseJsonArray(value) {
|
|
147
|
+
if (!value)
|
|
148
|
+
return [];
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(value);
|
|
151
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database schema — auto-migration for all sf_ tables
|
|
3
|
+
* Creates tables if they don't exist. All tables prefixed sf_ to avoid conflicts.
|
|
4
|
+
*/
|
|
5
|
+
import type { ConnectionPool } from 'mssql';
|
|
6
|
+
/**
|
|
7
|
+
* Run auto-migration: create all tables and indexes if they don't exist
|
|
8
|
+
*/
|
|
9
|
+
export declare function runMigrations(pool: ConnectionPool): Promise<void>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database schema — auto-migration for all sf_ tables
|
|
3
|
+
* Creates tables if they don't exist. All tables prefixed sf_ to avoid conflicts.
|
|
4
|
+
*/
|
|
5
|
+
const TABLES = [
|
|
6
|
+
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'sf_projects')
|
|
7
|
+
CREATE TABLE sf_projects (
|
|
8
|
+
id NVARCHAR(50) PRIMARY KEY,
|
|
9
|
+
name NVARCHAR(255) NOT NULL,
|
|
10
|
+
api_key NVARCHAR(100) UNIQUE NOT NULL,
|
|
11
|
+
description NVARCHAR(MAX),
|
|
12
|
+
allowed_origins NVARCHAR(MAX),
|
|
13
|
+
created_at BIGINT NOT NULL,
|
|
14
|
+
updated_at BIGINT NOT NULL
|
|
15
|
+
)`,
|
|
16
|
+
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'sf_systems')
|
|
17
|
+
CREATE TABLE sf_systems (
|
|
18
|
+
id NVARCHAR(50) PRIMARY KEY,
|
|
19
|
+
project_id NVARCHAR(50) NOT NULL REFERENCES sf_projects(id),
|
|
20
|
+
name NVARCHAR(255) NOT NULL,
|
|
21
|
+
description NVARCHAR(MAX),
|
|
22
|
+
status NVARCHAR(50) NOT NULL DEFAULT 'operational',
|
|
23
|
+
display_order INT DEFAULT 0,
|
|
24
|
+
created_at BIGINT NOT NULL,
|
|
25
|
+
updated_at BIGINT NOT NULL
|
|
26
|
+
)`,
|
|
27
|
+
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'sf_incidents')
|
|
28
|
+
CREATE TABLE sf_incidents (
|
|
29
|
+
id NVARCHAR(50) PRIMARY KEY,
|
|
30
|
+
project_id NVARCHAR(50) NOT NULL REFERENCES sf_projects(id),
|
|
31
|
+
title NVARCHAR(500) NOT NULL,
|
|
32
|
+
status NVARCHAR(50) NOT NULL DEFAULT 'investigating',
|
|
33
|
+
severity NVARCHAR(50) NOT NULL DEFAULT 'minor',
|
|
34
|
+
affected_systems NVARCHAR(MAX),
|
|
35
|
+
created_at BIGINT NOT NULL,
|
|
36
|
+
updated_at BIGINT NOT NULL,
|
|
37
|
+
resolved_at BIGINT
|
|
38
|
+
)`,
|
|
39
|
+
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'sf_incident_updates')
|
|
40
|
+
CREATE TABLE sf_incident_updates (
|
|
41
|
+
id NVARCHAR(50) PRIMARY KEY,
|
|
42
|
+
incident_id NVARCHAR(50) NOT NULL REFERENCES sf_incidents(id),
|
|
43
|
+
status NVARCHAR(50) NOT NULL,
|
|
44
|
+
message NVARCHAR(MAX) NOT NULL,
|
|
45
|
+
created_at BIGINT NOT NULL
|
|
46
|
+
)`,
|
|
47
|
+
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'sf_feedback')
|
|
48
|
+
CREATE TABLE sf_feedback (
|
|
49
|
+
id NVARCHAR(50) PRIMARY KEY,
|
|
50
|
+
project_id NVARCHAR(50) NOT NULL REFERENCES sf_projects(id),
|
|
51
|
+
category NVARCHAR(100),
|
|
52
|
+
message NVARCHAR(MAX) NOT NULL,
|
|
53
|
+
email NVARCHAR(255),
|
|
54
|
+
user_agent NVARCHAR(500),
|
|
55
|
+
page_url NVARCHAR(500),
|
|
56
|
+
metadata NVARCHAR(MAX),
|
|
57
|
+
status NVARCHAR(50) DEFAULT 'new',
|
|
58
|
+
created_at BIGINT NOT NULL
|
|
59
|
+
)`,
|
|
60
|
+
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'sf_status_history')
|
|
61
|
+
CREATE TABLE sf_status_history (
|
|
62
|
+
id NVARCHAR(50) PRIMARY KEY,
|
|
63
|
+
system_id NVARCHAR(50) NOT NULL REFERENCES sf_systems(id),
|
|
64
|
+
status NVARCHAR(50) NOT NULL,
|
|
65
|
+
changed_at BIGINT NOT NULL
|
|
66
|
+
)`,
|
|
67
|
+
];
|
|
68
|
+
const INDEXES = [
|
|
69
|
+
`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sf_systems_project')
|
|
70
|
+
CREATE INDEX idx_sf_systems_project ON sf_systems(project_id)`,
|
|
71
|
+
`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sf_incidents_project')
|
|
72
|
+
CREATE INDEX idx_sf_incidents_project ON sf_incidents(project_id)`,
|
|
73
|
+
`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sf_incident_updates_incident')
|
|
74
|
+
CREATE INDEX idx_sf_incident_updates_incident ON sf_incident_updates(incident_id)`,
|
|
75
|
+
`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sf_feedback_project')
|
|
76
|
+
CREATE INDEX idx_sf_feedback_project ON sf_feedback(project_id)`,
|
|
77
|
+
`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sf_feedback_status')
|
|
78
|
+
CREATE INDEX idx_sf_feedback_status ON sf_feedback(status)`,
|
|
79
|
+
`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sf_status_history_system')
|
|
80
|
+
CREATE INDEX idx_sf_status_history_system ON sf_status_history(system_id)`,
|
|
81
|
+
`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sf_projects_api_key')
|
|
82
|
+
CREATE INDEX idx_sf_projects_api_key ON sf_projects(api_key)`,
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* Run auto-migration: create all tables and indexes if they don't exist
|
|
86
|
+
*/
|
|
87
|
+
export async function runMigrations(pool) {
|
|
88
|
+
for (const sql of TABLES) {
|
|
89
|
+
await pool.request().query(sql);
|
|
90
|
+
}
|
|
91
|
+
for (const sql of INDEXES) {
|
|
92
|
+
await pool.request().query(sql);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Systems CRUD + status management
|
|
3
|
+
*/
|
|
4
|
+
import type { ConnectionPool } from 'mssql';
|
|
5
|
+
import type { System, SystemStatus, StatusHistoryEntry } from './types.js';
|
|
6
|
+
export declare function createSystem(pool: ConnectionPool, projectId: string, data: {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
displayOrder?: number;
|
|
10
|
+
}): Promise<System>;
|
|
11
|
+
export declare function getSystemById(pool: ConnectionPool, id: string): Promise<System | null>;
|
|
12
|
+
export declare function getSystemsByProject(pool: ConnectionPool, projectId: string): Promise<System[]>;
|
|
13
|
+
export declare function updateSystem(pool: ConnectionPool, id: string, data: {
|
|
14
|
+
name?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
displayOrder?: number;
|
|
17
|
+
}): Promise<System | null>;
|
|
18
|
+
export declare function deleteSystem(pool: ConnectionPool, id: string): Promise<boolean>;
|
|
19
|
+
export declare function updateSystemStatus(pool: ConnectionPool, id: string, status: SystemStatus): Promise<System | null>;
|
|
20
|
+
export declare function getStatusHistory(pool: ConnectionPool, systemId: string, days?: number): Promise<StatusHistoryEntry[]>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Systems CRUD + status management
|
|
3
|
+
*/
|
|
4
|
+
import { nanoid } from 'nanoid';
|
|
5
|
+
// ── CRUD ──
|
|
6
|
+
export async function createSystem(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('name', data.name)
|
|
14
|
+
.input('description', data.description || null)
|
|
15
|
+
.input('displayOrder', data.displayOrder ?? 0)
|
|
16
|
+
.input('createdAt', now)
|
|
17
|
+
.input('updatedAt', now)
|
|
18
|
+
.query(`
|
|
19
|
+
INSERT INTO sf_systems (id, project_id, name, description, display_order, created_at, updated_at)
|
|
20
|
+
VALUES (@id, @projectId, @name, @description, @displayOrder, @createdAt, @updatedAt)
|
|
21
|
+
`);
|
|
22
|
+
// Record initial status in history
|
|
23
|
+
await recordStatusChange(pool, id, 'operational', now);
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
projectId,
|
|
27
|
+
name: data.name,
|
|
28
|
+
description: data.description || null,
|
|
29
|
+
status: 'operational',
|
|
30
|
+
displayOrder: data.displayOrder ?? 0,
|
|
31
|
+
createdAt: now,
|
|
32
|
+
updatedAt: now,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function getSystemById(pool, id) {
|
|
36
|
+
const result = await pool
|
|
37
|
+
.request()
|
|
38
|
+
.input('id', id)
|
|
39
|
+
.query('SELECT * FROM sf_systems WHERE id = @id');
|
|
40
|
+
if (result.recordset.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return mapSystemRow(result.recordset[0]);
|
|
43
|
+
}
|
|
44
|
+
export async function getSystemsByProject(pool, projectId) {
|
|
45
|
+
const result = await pool
|
|
46
|
+
.request()
|
|
47
|
+
.input('projectId', projectId)
|
|
48
|
+
.query('SELECT * FROM sf_systems WHERE project_id = @projectId ORDER BY display_order ASC, created_at ASC');
|
|
49
|
+
return result.recordset.map(mapSystemRow);
|
|
50
|
+
}
|
|
51
|
+
export async function updateSystem(pool, id, data) {
|
|
52
|
+
const existing = await getSystemById(pool, id);
|
|
53
|
+
if (!existing)
|
|
54
|
+
return null;
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
await pool
|
|
57
|
+
.request()
|
|
58
|
+
.input('id', id)
|
|
59
|
+
.input('name', data.name ?? existing.name)
|
|
60
|
+
.input('description', data.description ?? existing.description)
|
|
61
|
+
.input('displayOrder', data.displayOrder ?? existing.displayOrder)
|
|
62
|
+
.input('updatedAt', now)
|
|
63
|
+
.query(`
|
|
64
|
+
UPDATE sf_systems
|
|
65
|
+
SET name = @name, description = @description, display_order = @displayOrder, updated_at = @updatedAt
|
|
66
|
+
WHERE id = @id
|
|
67
|
+
`);
|
|
68
|
+
return {
|
|
69
|
+
...existing,
|
|
70
|
+
name: data.name ?? existing.name,
|
|
71
|
+
description: data.description ?? existing.description,
|
|
72
|
+
displayOrder: data.displayOrder ?? existing.displayOrder,
|
|
73
|
+
updatedAt: now,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function deleteSystem(pool, id) {
|
|
77
|
+
await pool.request().input('id', id).query(`
|
|
78
|
+
DELETE FROM sf_status_history WHERE system_id = @id;
|
|
79
|
+
DELETE FROM sf_systems WHERE id = @id;
|
|
80
|
+
`);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
// ── Status Management ──
|
|
84
|
+
export async function updateSystemStatus(pool, id, status) {
|
|
85
|
+
const existing = await getSystemById(pool, id);
|
|
86
|
+
if (!existing)
|
|
87
|
+
return null;
|
|
88
|
+
if (existing.status === status)
|
|
89
|
+
return existing;
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
await pool
|
|
92
|
+
.request()
|
|
93
|
+
.input('id', id)
|
|
94
|
+
.input('status', status)
|
|
95
|
+
.input('updatedAt', now)
|
|
96
|
+
.query('UPDATE sf_systems SET status = @status, updated_at = @updatedAt WHERE id = @id');
|
|
97
|
+
await recordStatusChange(pool, id, status, now);
|
|
98
|
+
return { ...existing, status, updatedAt: now };
|
|
99
|
+
}
|
|
100
|
+
export async function getStatusHistory(pool, systemId, days = 90) {
|
|
101
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
102
|
+
const result = await pool
|
|
103
|
+
.request()
|
|
104
|
+
.input('systemId', systemId)
|
|
105
|
+
.input('since', since)
|
|
106
|
+
.query(`
|
|
107
|
+
SELECT * FROM sf_status_history
|
|
108
|
+
WHERE system_id = @systemId AND changed_at >= @since
|
|
109
|
+
ORDER BY changed_at ASC
|
|
110
|
+
`);
|
|
111
|
+
return result.recordset.map(mapHistoryRow);
|
|
112
|
+
}
|
|
113
|
+
// ── Helpers ──
|
|
114
|
+
async function recordStatusChange(pool, systemId, status, changedAt) {
|
|
115
|
+
const id = nanoid(16);
|
|
116
|
+
await pool
|
|
117
|
+
.request()
|
|
118
|
+
.input('id', id)
|
|
119
|
+
.input('systemId', systemId)
|
|
120
|
+
.input('status', status)
|
|
121
|
+
.input('changedAt', changedAt)
|
|
122
|
+
.query(`
|
|
123
|
+
INSERT INTO sf_status_history (id, system_id, status, changed_at)
|
|
124
|
+
VALUES (@id, @systemId, @status, @changedAt)
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
function mapSystemRow(row) {
|
|
128
|
+
return {
|
|
129
|
+
id: row.id,
|
|
130
|
+
projectId: row.project_id,
|
|
131
|
+
name: row.name,
|
|
132
|
+
description: row.description || null,
|
|
133
|
+
status: row.status,
|
|
134
|
+
displayOrder: row.display_order,
|
|
135
|
+
createdAt: row.created_at,
|
|
136
|
+
updatedAt: row.updated_at,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function mapHistoryRow(row) {
|
|
140
|
+
return {
|
|
141
|
+
id: row.id,
|
|
142
|
+
systemId: row.system_id,
|
|
143
|
+
status: row.status,
|
|
144
|
+
changedAt: row.changed_at,
|
|
145
|
+
};
|
|
146
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the status-feedback-system package
|
|
3
|
+
*/
|
|
4
|
+
export type SystemStatus = 'operational' | 'degraded' | 'outage' | 'maintenance';
|
|
5
|
+
export type IncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved';
|
|
6
|
+
export type IncidentSeverity = 'minor' | 'major' | 'critical';
|
|
7
|
+
export type FeedbackStatus = 'new' | 'reviewed' | 'resolved' | 'archived';
|
|
8
|
+
export interface Project {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
description: string | null;
|
|
13
|
+
allowedOrigins: string[];
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface System {
|
|
18
|
+
id: string;
|
|
19
|
+
projectId: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string | null;
|
|
22
|
+
status: SystemStatus;
|
|
23
|
+
displayOrder: number;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Incident {
|
|
28
|
+
id: string;
|
|
29
|
+
projectId: string;
|
|
30
|
+
title: string;
|
|
31
|
+
status: IncidentStatus;
|
|
32
|
+
severity: IncidentSeverity;
|
|
33
|
+
affectedSystems: string[];
|
|
34
|
+
createdAt: number;
|
|
35
|
+
updatedAt: number;
|
|
36
|
+
resolvedAt: number | null;
|
|
37
|
+
updates?: IncidentUpdate[];
|
|
38
|
+
}
|
|
39
|
+
export interface IncidentUpdate {
|
|
40
|
+
id: string;
|
|
41
|
+
incidentId: string;
|
|
42
|
+
status: IncidentStatus;
|
|
43
|
+
message: string;
|
|
44
|
+
createdAt: number;
|
|
45
|
+
}
|
|
46
|
+
export interface Feedback {
|
|
47
|
+
id: string;
|
|
48
|
+
projectId: string;
|
|
49
|
+
category: string | null;
|
|
50
|
+
message: string;
|
|
51
|
+
email: string | null;
|
|
52
|
+
userAgent: string | null;
|
|
53
|
+
pageUrl: string | null;
|
|
54
|
+
metadata: Record<string, unknown> | null;
|
|
55
|
+
status: FeedbackStatus;
|
|
56
|
+
createdAt: number;
|
|
57
|
+
}
|
|
58
|
+
export interface StatusHistoryEntry {
|
|
59
|
+
id: string;
|
|
60
|
+
systemId: string;
|
|
61
|
+
status: SystemStatus;
|
|
62
|
+
changedAt: number;
|
|
63
|
+
}
|
|
64
|
+
export interface StatusServerConfig {
|
|
65
|
+
database: {
|
|
66
|
+
connectionString: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface ApiResponse<T = unknown> {
|
|
70
|
+
success: boolean;
|
|
71
|
+
data?: T;
|
|
72
|
+
error?: string;
|
|
73
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alliance-droid/status-feedback-system",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
|
|
6
|
+
"svelte": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"svelte": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./server": {
|
|
14
|
+
"types": "./dist/server/index.d.ts",
|
|
15
|
+
"default": "./dist/server/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./admin": {
|
|
18
|
+
"types": "./dist/components/admin/index.d.ts",
|
|
19
|
+
"svelte": "./dist/components/admin/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"!dist/**/*.test.*"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "vite dev",
|
|
28
|
+
"build": "vite build && npm run package",
|
|
29
|
+
"package": "svelte-kit sync && svelte-package -o dist",
|
|
30
|
+
"preview": "vite preview",
|
|
31
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"prepublishOnly": "npm run package"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"svelte": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@alliance-droid/svelte-component-library": "^0.1.27",
|
|
41
|
+
"mssql": "^11.0.0",
|
|
42
|
+
"nanoid": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
46
|
+
"@sveltejs/kit": "^2.50.0",
|
|
47
|
+
"@sveltejs/package": "^2.3.0",
|
|
48
|
+
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
|
49
|
+
"@tailwindcss/vite": "^4.1.0",
|
|
50
|
+
"@types/mssql": "^9.1.0",
|
|
51
|
+
"publint": "^0.3.0",
|
|
52
|
+
"svelte": "^5.48.0",
|
|
53
|
+
"svelte-check": "^4.3.0",
|
|
54
|
+
"tailwindcss": "^4.1.0",
|
|
55
|
+
"typescript": "^5.9.0",
|
|
56
|
+
"vite": "^7.3.0",
|
|
57
|
+
"vitest": "^4.0.0"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"svelte",
|
|
61
|
+
"sveltekit",
|
|
62
|
+
"status-page",
|
|
63
|
+
"feedback",
|
|
64
|
+
"uptime"
|
|
65
|
+
],
|
|
66
|
+
"author": "Alliance Technical Group",
|
|
67
|
+
"license": "UNLICENSED",
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "restricted"
|
|
70
|
+
}
|
|
71
|
+
}
|