@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,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SystemStatus } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
status: SystemStatus;
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
showLabel?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { status, size = 'md', showLabel = false }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const config: Record<SystemStatus, { label: string; icon: string; colorClass: string }> = {
|
|
13
|
+
operational: {
|
|
14
|
+
label: 'Operational',
|
|
15
|
+
icon: 'fa-circle-check',
|
|
16
|
+
colorClass: 'text-green-500',
|
|
17
|
+
},
|
|
18
|
+
degraded: {
|
|
19
|
+
label: 'Degraded',
|
|
20
|
+
icon: 'fa-exclamation-triangle',
|
|
21
|
+
colorClass: 'text-yellow-500',
|
|
22
|
+
},
|
|
23
|
+
outage: {
|
|
24
|
+
label: 'Outage',
|
|
25
|
+
icon: 'fa-circle-xmark',
|
|
26
|
+
colorClass: 'text-red-500',
|
|
27
|
+
},
|
|
28
|
+
maintenance: {
|
|
29
|
+
label: 'Maintenance',
|
|
30
|
+
icon: 'fa-wrench',
|
|
31
|
+
colorClass: 'text-blue-500',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const sizeClasses: Record<string, string> = {
|
|
36
|
+
sm: 'text-xs',
|
|
37
|
+
md: 'text-sm',
|
|
38
|
+
lg: 'text-base',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let current = $derived(config[status]);
|
|
42
|
+
let sizeClass = $derived(sizeClasses[size]);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<span class="inline-flex items-center gap-1.5 {sizeClass}">
|
|
46
|
+
<i class="fa-solid {current.icon} {current.colorClass}"></i>
|
|
47
|
+
{#if showLabel}
|
|
48
|
+
<span class="text-foreground-secondary">{current.label}</span>
|
|
49
|
+
{/if}
|
|
50
|
+
</span>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SystemStatus } from '../../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
status: SystemStatus;
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
showLabel?: boolean;
|
|
6
|
+
}
|
|
7
|
+
declare const StatusIndicator: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type StatusIndicator = ReturnType<typeof StatusIndicator>;
|
|
9
|
+
export default StatusIndicator;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { System, StatusHistoryEntry } from '../../types.js';
|
|
3
|
+
import StatusIndicator from './StatusIndicator.svelte';
|
|
4
|
+
import UptimeBar from './UptimeBar.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
system: System;
|
|
8
|
+
history?: StatusHistoryEntry[];
|
|
9
|
+
showUptime?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { system, history = [], showUptime = true }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="rounded-lg border border-border bg-surface p-4">
|
|
16
|
+
<div class="flex items-center justify-between">
|
|
17
|
+
<div>
|
|
18
|
+
<h3 class="text-sm font-semibold text-foreground">{system.name}</h3>
|
|
19
|
+
{#if system.description}
|
|
20
|
+
<p class="mt-0.5 text-xs text-foreground-tertiary">{system.description}</p>
|
|
21
|
+
{/if}
|
|
22
|
+
</div>
|
|
23
|
+
<StatusIndicator status={system.status} showLabel size="sm" />
|
|
24
|
+
</div>
|
|
25
|
+
{#if showUptime && history.length > 0}
|
|
26
|
+
<div class="mt-3">
|
|
27
|
+
<UptimeBar {history} />
|
|
28
|
+
</div>
|
|
29
|
+
{/if}
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { System, StatusHistoryEntry } from '../../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
system: System;
|
|
4
|
+
history?: StatusHistoryEntry[];
|
|
5
|
+
showUptime?: boolean;
|
|
6
|
+
}
|
|
7
|
+
declare const SystemCard: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type SystemCard = ReturnType<typeof SystemCard>;
|
|
9
|
+
export default SystemCard;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { StatusHistoryEntry, SystemStatus } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
history: StatusHistoryEntry[];
|
|
6
|
+
days?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { history, days = 90 }: Props = $props();
|
|
10
|
+
|
|
11
|
+
const statusColors: Record<SystemStatus, string> = {
|
|
12
|
+
operational: '#22c55e',
|
|
13
|
+
degraded: '#eab308',
|
|
14
|
+
outage: '#ef4444',
|
|
15
|
+
maintenance: '#3b82f6',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build day-by-day status bars from history entries.
|
|
20
|
+
* Each day gets the "worst" status that occurred during it.
|
|
21
|
+
*/
|
|
22
|
+
let dayBars = $derived.by(() => {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const msPerDay = 24 * 60 * 60 * 1000;
|
|
25
|
+
const bars: { date: string; status: SystemStatus; color: string }[] = [];
|
|
26
|
+
|
|
27
|
+
const severity: Record<SystemStatus, number> = {
|
|
28
|
+
operational: 0,
|
|
29
|
+
maintenance: 1,
|
|
30
|
+
degraded: 2,
|
|
31
|
+
outage: 3,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
35
|
+
const dayStart = now - (i + 1) * msPerDay;
|
|
36
|
+
const dayEnd = now - i * msPerDay;
|
|
37
|
+
const dateStr = new Date(dayEnd).toLocaleDateString();
|
|
38
|
+
|
|
39
|
+
// Find worst status that overlapped with this day
|
|
40
|
+
let worstStatus: SystemStatus = 'operational';
|
|
41
|
+
let worstSeverity = 0;
|
|
42
|
+
|
|
43
|
+
for (const entry of history) {
|
|
44
|
+
if (entry.changedAt <= dayEnd && entry.changedAt >= dayStart - msPerDay) {
|
|
45
|
+
const s = severity[entry.status] ?? 0;
|
|
46
|
+
if (s > worstSeverity) {
|
|
47
|
+
worstSeverity = s;
|
|
48
|
+
worstStatus = entry.status;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
bars.push({
|
|
54
|
+
date: dateStr,
|
|
55
|
+
status: worstStatus,
|
|
56
|
+
color: statusColors[worstStatus],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return bars;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let uptimePercent = $derived.by(() => {
|
|
64
|
+
if (dayBars.length === 0) return 100;
|
|
65
|
+
const operational = dayBars.filter((d) => d.status === 'operational').length;
|
|
66
|
+
return Math.round((operational / dayBars.length) * 1000) / 10;
|
|
67
|
+
});
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div class="space-y-1">
|
|
71
|
+
<div class="flex items-center justify-between text-xs text-foreground-tertiary">
|
|
72
|
+
<span>{days} days ago</span>
|
|
73
|
+
<span>{uptimePercent}% uptime</span>
|
|
74
|
+
<span>Today</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="flex gap-px">
|
|
77
|
+
{#each dayBars as bar}
|
|
78
|
+
<div
|
|
79
|
+
class="h-8 flex-1 rounded-sm transition-opacity hover:opacity-80"
|
|
80
|
+
style="background-color: {bar.color}"
|
|
81
|
+
title="{bar.date}: {bar.status}"
|
|
82
|
+
></div>
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StatusHistoryEntry } from '../../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
history: StatusHistoryEntry[];
|
|
4
|
+
days?: number;
|
|
5
|
+
}
|
|
6
|
+
declare const UptimeBar: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type UptimeBar = ReturnType<typeof UptimeBar>;
|
|
8
|
+
export default UptimeBar;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @alliance-droid/status-feedback-system
|
|
3
|
+
*
|
|
4
|
+
* Drop-in system status pages and user feedback forms for SvelteKit apps.
|
|
5
|
+
*
|
|
6
|
+
* Client exports (components):
|
|
7
|
+
* import { StatusPage, FeedbackForm } from '@alliance-droid/status-feedback-system';
|
|
8
|
+
*
|
|
9
|
+
* Server exports:
|
|
10
|
+
* import { createStatusServer } from '@alliance-droid/status-feedback-system/server';
|
|
11
|
+
*
|
|
12
|
+
* Admin exports:
|
|
13
|
+
* import { AdminDashboard } from '@alliance-droid/status-feedback-system/admin';
|
|
14
|
+
*/
|
|
15
|
+
export type { Project, System, Incident, IncidentUpdate, Feedback, StatusHistoryEntry, SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus, StatusServerConfig, ApiResponse, } from './types.js';
|
|
16
|
+
export { default as StatusPage } from './components/StatusPage.svelte';
|
|
17
|
+
export { default as StatusBanner } from './components/StatusBanner.svelte';
|
|
18
|
+
export { default as FeedbackForm } from './components/FeedbackForm.svelte';
|
|
19
|
+
export { default as FeedbackButton } from './components/FeedbackButton.svelte';
|
|
20
|
+
export { default as StatusIndicator } from './components/shared/StatusIndicator.svelte';
|
|
21
|
+
export { default as UptimeBar } from './components/shared/UptimeBar.svelte';
|
|
22
|
+
export { default as IncidentTimeline } from './components/shared/IncidentTimeline.svelte';
|
|
23
|
+
export { default as SystemCard } from './components/shared/SystemCard.svelte';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @alliance-droid/status-feedback-system
|
|
3
|
+
*
|
|
4
|
+
* Drop-in system status pages and user feedback forms for SvelteKit apps.
|
|
5
|
+
*
|
|
6
|
+
* Client exports (components):
|
|
7
|
+
* import { StatusPage, FeedbackForm } from '@alliance-droid/status-feedback-system';
|
|
8
|
+
*
|
|
9
|
+
* Server exports:
|
|
10
|
+
* import { createStatusServer } from '@alliance-droid/status-feedback-system/server';
|
|
11
|
+
*
|
|
12
|
+
* Admin exports:
|
|
13
|
+
* import { AdminDashboard } from '@alliance-droid/status-feedback-system/admin';
|
|
14
|
+
*/
|
|
15
|
+
// ── Client Components ──
|
|
16
|
+
export { default as StatusPage } from './components/StatusPage.svelte';
|
|
17
|
+
export { default as StatusBanner } from './components/StatusBanner.svelte';
|
|
18
|
+
export { default as FeedbackForm } from './components/FeedbackForm.svelte';
|
|
19
|
+
export { default as FeedbackButton } from './components/FeedbackButton.svelte';
|
|
20
|
+
// ── Shared Components ──
|
|
21
|
+
export { default as StatusIndicator } from './components/shared/StatusIndicator.svelte';
|
|
22
|
+
export { default as UptimeBar } from './components/shared/UptimeBar.svelte';
|
|
23
|
+
export { default as IncidentTimeline } from './components/shared/IncidentTimeline.svelte';
|
|
24
|
+
export { default as SystemCard } from './components/shared/SystemCard.svelte';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection management
|
|
3
|
+
* Provides a singleton connection pool for MSSQL
|
|
4
|
+
*/
|
|
5
|
+
import type { ConnectionPool } from 'mssql';
|
|
6
|
+
/**
|
|
7
|
+
* Get the database connection pool. Creates one if it doesn't exist.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getPool(connectionString: string): Promise<ConnectionPool>;
|
|
10
|
+
/**
|
|
11
|
+
* Initialize the database: connect and run migrations
|
|
12
|
+
*/
|
|
13
|
+
export declare function initDatabase(connectionString: string): Promise<ConnectionPool>;
|
|
14
|
+
/**
|
|
15
|
+
* Close the database connection
|
|
16
|
+
*/
|
|
17
|
+
export declare function closeDatabase(): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection management
|
|
3
|
+
* Provides a singleton connection pool for MSSQL
|
|
4
|
+
*/
|
|
5
|
+
import sql from 'mssql';
|
|
6
|
+
import { runMigrations } from './schema.js';
|
|
7
|
+
let _pool = null;
|
|
8
|
+
let _initialized = false;
|
|
9
|
+
/**
|
|
10
|
+
* Parse a connection string into mssql config
|
|
11
|
+
*/
|
|
12
|
+
function parseConnectionString(connectionString) {
|
|
13
|
+
const params = {};
|
|
14
|
+
for (const part of connectionString.split(';')) {
|
|
15
|
+
const [key, ...valueParts] = part.split('=');
|
|
16
|
+
if (key && valueParts.length > 0) {
|
|
17
|
+
params[key.trim().toLowerCase()] = valueParts.join('=').trim();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
server: params['server'] || params['data source'] || '',
|
|
22
|
+
database: params['initial catalog'] || params['database'] || '',
|
|
23
|
+
user: params['user id'] || params['uid'] || '',
|
|
24
|
+
password: params['password'] || params['pwd'] || '',
|
|
25
|
+
options: {
|
|
26
|
+
encrypt: params['encrypt']?.toLowerCase() !== 'false',
|
|
27
|
+
trustServerCertificate: params['trustservercertificate']?.toLowerCase() === 'true',
|
|
28
|
+
},
|
|
29
|
+
pool: {
|
|
30
|
+
max: 10,
|
|
31
|
+
min: 0,
|
|
32
|
+
idleTimeoutMillis: 30000,
|
|
33
|
+
},
|
|
34
|
+
requestTimeout: parseInt(params['connection timeout'] || '30') * 1000,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the database connection pool. Creates one if it doesn't exist.
|
|
39
|
+
*/
|
|
40
|
+
export async function getPool(connectionString) {
|
|
41
|
+
if (_pool?.connected) {
|
|
42
|
+
return _pool;
|
|
43
|
+
}
|
|
44
|
+
const config = parseConnectionString(connectionString);
|
|
45
|
+
_pool = await sql.connect(config);
|
|
46
|
+
return _pool;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the database: connect and run migrations
|
|
50
|
+
*/
|
|
51
|
+
export async function initDatabase(connectionString) {
|
|
52
|
+
const pool = await getPool(connectionString);
|
|
53
|
+
if (!_initialized) {
|
|
54
|
+
await runMigrations(pool);
|
|
55
|
+
_initialized = true;
|
|
56
|
+
}
|
|
57
|
+
return pool;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Close the database connection
|
|
61
|
+
*/
|
|
62
|
+
export async function closeDatabase() {
|
|
63
|
+
if (_pool) {
|
|
64
|
+
await _pool.close();
|
|
65
|
+
_pool = null;
|
|
66
|
+
_initialized = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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>;
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 {};
|