@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.
Files changed (54) hide show
  1. package/dist/api.d.ts +94 -0
  2. package/dist/api.js +100 -0
  3. package/dist/components/FeedbackBoard.svelte +842 -0
  4. package/dist/components/FeedbackBoard.svelte.d.ts +19 -0
  5. package/dist/components/FeedbackButton.svelte +3 -3
  6. package/dist/components/FeedbackButton.svelte.d.ts +1 -1
  7. package/dist/components/FeedbackForm.svelte +3 -3
  8. package/dist/components/FeedbackForm.svelte.d.ts +1 -1
  9. package/dist/components/StatusBanner.svelte +36 -3
  10. package/dist/components/StatusBanner.svelte.d.ts +7 -1
  11. package/dist/components/StatusPage.svelte +150 -83
  12. package/dist/components/StatusPage.svelte.d.ts +10 -3
  13. package/dist/index.d.ts +7 -9
  14. package/dist/index.js +6 -8
  15. package/dist/types.d.ts +13 -6
  16. package/package.json +2 -13
  17. package/dist/components/admin/AdminDashboard.svelte +0 -274
  18. package/dist/components/admin/AdminDashboard.svelte.d.ts +0 -9
  19. package/dist/components/admin/ApiKeyManager.svelte +0 -68
  20. package/dist/components/admin/ApiKeyManager.svelte.d.ts +0 -7
  21. package/dist/components/admin/FeedbackList.svelte +0 -160
  22. package/dist/components/admin/FeedbackList.svelte.d.ts +0 -17
  23. package/dist/components/admin/IncidentForm.svelte +0 -135
  24. package/dist/components/admin/IncidentForm.svelte.d.ts +0 -14
  25. package/dist/components/admin/IncidentManager.svelte +0 -153
  26. package/dist/components/admin/IncidentManager.svelte.d.ts +0 -13
  27. package/dist/components/admin/ProjectForm.svelte +0 -106
  28. package/dist/components/admin/ProjectForm.svelte.d.ts +0 -13
  29. package/dist/components/admin/ProjectList.svelte +0 -57
  30. package/dist/components/admin/ProjectList.svelte.d.ts +0 -10
  31. package/dist/components/admin/StatusControl.svelte +0 -62
  32. package/dist/components/admin/StatusControl.svelte.d.ts +0 -8
  33. package/dist/components/admin/SystemManager.svelte +0 -119
  34. package/dist/components/admin/SystemManager.svelte.d.ts +0 -13
  35. package/dist/components/admin/index.d.ts +0 -9
  36. package/dist/components/admin/index.js +0 -9
  37. package/dist/server/db.d.ts +0 -17
  38. package/dist/server/db.js +0 -68
  39. package/dist/server/feedback.d.ts +0 -27
  40. package/dist/server/feedback.js +0 -124
  41. package/dist/server/handlers.d.ts +0 -41
  42. package/dist/server/handlers.js +0 -251
  43. package/dist/server/incidents.d.ts +0 -27
  44. package/dist/server/incidents.js +0 -180
  45. package/dist/server/index.d.ts +0 -101
  46. package/dist/server/index.js +0 -71
  47. package/dist/server/projects.d.ts +0 -45
  48. package/dist/server/projects.js +0 -156
  49. package/dist/server/schema.d.ts +0 -9
  50. package/dist/server/schema.js +0 -94
  51. package/dist/server/systems.d.ts +0 -20
  52. package/dist/server/systems.js +0 -146
  53. package/dist/server/types.d.ts +0 -4
  54. package/dist/server/types.js +0 -4
@@ -1,274 +0,0 @@
1
- <script lang="ts">
2
- import type { Project, System, Incident, Feedback, FeedbackStatus, SystemStatus, IncidentStatus, IncidentSeverity } from '../../types.js';
3
- import ProjectList from './ProjectList.svelte';
4
- import ProjectForm from './ProjectForm.svelte';
5
- import ApiKeyManager from './ApiKeyManager.svelte';
6
- import SystemManager from './SystemManager.svelte';
7
- import StatusControl from './StatusControl.svelte';
8
- import IncidentManager from './IncidentManager.svelte';
9
- import IncidentForm from './IncidentForm.svelte';
10
- import FeedbackList from './FeedbackList.svelte';
11
-
12
- interface Props {
13
- projects: Project[];
14
- apiEndpoint: string;
15
- onRefresh?: () => Promise<void>;
16
- }
17
-
18
- let { projects, apiEndpoint, onRefresh }: Props = $props();
19
-
20
- type Tab = 'overview' | 'systems' | 'incidents' | 'feedback';
21
-
22
- let selectedProject = $state<Project | null>(null);
23
- let activeTab = $state<Tab>('overview');
24
- let showProjectForm = $state(false);
25
- let showIncidentForm = $state(false);
26
-
27
- // Data for selected project
28
- let systems = $state<System[]>([]);
29
- let incidents = $state<Incident[]>([]);
30
- let feedbackItems = $state<Feedback[]>([]);
31
- let feedbackTotal = $state(0);
32
- let feedbackPage = $state(1);
33
- let feedbackFilterStatus = $state<FeedbackStatus | ''>('');
34
- let loading = $state(false);
35
-
36
- const tabs: { id: Tab; label: string; icon: string }[] = [
37
- { id: 'overview', label: 'Overview', icon: 'fa-gauge' },
38
- { id: 'systems', label: 'Systems', icon: 'fa-server' },
39
- { id: 'incidents', label: 'Incidents', icon: 'fa-triangle-exclamation' },
40
- { id: 'feedback', label: 'Feedback', icon: 'fa-comment' },
41
- ];
42
-
43
- // ── API Helpers ──
44
-
45
- async function apiCall(path: string, options: RequestInit = {}) {
46
- if (!selectedProject) throw new Error('No project selected');
47
- const res = await fetch(`${apiEndpoint}/${path}`, {
48
- ...options,
49
- headers: {
50
- 'Content-Type': 'application/json',
51
- 'x-api-key': selectedProject.apiKey,
52
- ...options.headers,
53
- },
54
- });
55
- const data = await res.json();
56
- if (!data.success) throw new Error(data.error || 'API error');
57
- return data.data;
58
- }
59
-
60
- async function loadProjectData() {
61
- if (!selectedProject) return;
62
- loading = true;
63
- try {
64
- systems = await apiCall('systems');
65
- incidents = await apiCall('incidents?limit=50&includeResolved=true');
66
- await loadFeedback();
67
- } catch (err) {
68
- console.error('Failed to load project data:', err);
69
- } finally {
70
- loading = false;
71
- }
72
- }
73
-
74
- async function loadFeedback() {
75
- const params = new URLSearchParams({
76
- limit: '20',
77
- offset: String((feedbackPage - 1) * 20),
78
- });
79
- if (feedbackFilterStatus) params.set('status', feedbackFilterStatus);
80
- const result = await apiCall(`feedback?${params}`);
81
- feedbackItems = result.items;
82
- feedbackTotal = result.total;
83
- }
84
-
85
- function selectProject(project: Project) {
86
- selectedProject = project;
87
- activeTab = 'overview';
88
- loadProjectData();
89
- }
90
-
91
- // ── Handlers ──
92
-
93
- async function handleCreateProject(data: { name: string; description: string; allowedOrigins: string[] }) {
94
- // This needs the consuming app to provide a server action
95
- // For now, emit to onRefresh
96
- showProjectForm = false;
97
- await onRefresh?.();
98
- }
99
-
100
- async function handleAddSystem(data: { name: string; description: string }) {
101
- // Systems are managed via the API route handlers
102
- await apiCall('systems', {
103
- method: 'POST',
104
- body: JSON.stringify(data),
105
- }).catch(() => {
106
- // Fallback: reload from server
107
- });
108
- await loadProjectData();
109
- }
110
-
111
- async function handleUpdateStatus(systemId: string, status: SystemStatus) {
112
- await apiCall(`systems/${systemId}/status`, {
113
- method: 'PATCH',
114
- body: JSON.stringify({ status }),
115
- });
116
- systems = systems.map((s) => (s.id === systemId ? { ...s, status } : s));
117
- }
118
-
119
- async function handleDeleteSystem(systemId: string) {
120
- await apiCall(`systems/${systemId}`, { method: 'DELETE' }).catch(() => {});
121
- await loadProjectData();
122
- }
123
-
124
- async function handleCreateIncident(data: { title: string; severity: IncidentSeverity; affectedSystems: string[]; message: string }) {
125
- await apiCall('incidents', {
126
- method: 'POST',
127
- body: JSON.stringify(data),
128
- }).catch(() => {});
129
- showIncidentForm = false;
130
- await loadProjectData();
131
- }
132
-
133
- async function handleAddIncidentUpdate(incidentId: string, data: { status: IncidentStatus; message: string }) {
134
- await apiCall(`incidents/${incidentId}/updates`, {
135
- method: 'POST',
136
- body: JSON.stringify(data),
137
- }).catch(() => {});
138
- await loadProjectData();
139
- }
140
-
141
- async function handleDeleteIncident(incidentId: string) {
142
- await apiCall(`incidents/${incidentId}`, { method: 'DELETE' }).catch(() => {});
143
- await loadProjectData();
144
- }
145
-
146
- async function handleFeedbackStatusChange(feedbackId: string, status: FeedbackStatus) {
147
- await apiCall(`feedback/${feedbackId}/status`, {
148
- method: 'PATCH',
149
- body: JSON.stringify({ status }),
150
- });
151
- feedbackItems = feedbackItems.map((f) => (f.id === feedbackId ? { ...f, status } : f));
152
- }
153
-
154
- async function handleDeleteFeedback(feedbackId: string) {
155
- if (!confirm('Delete this feedback?')) return;
156
- await apiCall(`feedback/${feedbackId}`, { method: 'DELETE' }).catch(() => {});
157
- await loadFeedback();
158
- }
159
- </script>
160
-
161
- <div class="flex min-h-screen">
162
- <!-- Sidebar -->
163
- <div class="w-64 border-r border-border bg-background p-4">
164
- <ProjectList
165
- {projects}
166
- selectedId={selectedProject?.id}
167
- onSelect={selectProject}
168
- onCreate={() => (showProjectForm = true)}
169
- />
170
- </div>
171
-
172
- <!-- Main Content -->
173
- <div class="flex-1 p-6">
174
- {#if showProjectForm}
175
- <div class="mx-auto max-w-lg">
176
- <ProjectForm
177
- onSave={handleCreateProject}
178
- onCancel={() => (showProjectForm = false)}
179
- />
180
- </div>
181
- {:else if !selectedProject}
182
- <div class="flex h-full items-center justify-center">
183
- <div class="text-center">
184
- <i class="fa-solid fa-arrow-left mb-2 text-2xl text-foreground-tertiary"></i>
185
- <p class="text-foreground-secondary">Select a project to manage</p>
186
- </div>
187
- </div>
188
- {:else}
189
- <!-- Project Header -->
190
- <div class="mb-6">
191
- <h2 class="text-xl font-semibold text-foreground">{selectedProject.name}</h2>
192
- {#if selectedProject.description}
193
- <p class="mt-1 text-sm text-foreground-tertiary">{selectedProject.description}</p>
194
- {/if}
195
- </div>
196
-
197
- <!-- API Key -->
198
- <div class="mb-6">
199
- <ApiKeyManager apiKey={selectedProject.apiKey} />
200
- </div>
201
-
202
- <!-- Tabs -->
203
- <div class="mb-6 flex gap-1 border-b border-border">
204
- {#each tabs as tab}
205
- <button
206
- onclick={() => (activeTab = tab.id)}
207
- class="border-b-2 px-4 py-2 text-sm font-semibold transition-colors {activeTab === tab.id
208
- ? 'border-primary text-primary'
209
- : 'border-transparent text-foreground-tertiary hover:text-foreground-secondary'}"
210
- >
211
- <i class="fa-solid {tab.icon} mr-1.5"></i>
212
- {tab.label}
213
- </button>
214
- {/each}
215
- </div>
216
-
217
- {#if loading}
218
- <div class="flex items-center justify-center py-12">
219
- <i class="fa-solid fa-spinner fa-spin text-xl text-foreground-tertiary"></i>
220
- </div>
221
- {:else if activeTab === 'overview'}
222
- <div class="space-y-6">
223
- <StatusControl {systems} onUpdateStatus={handleUpdateStatus} />
224
- <IncidentManager
225
- incidents={incidents.filter((i) => i.status !== 'resolved').slice(0, 5)}
226
- onAddUpdate={handleAddIncidentUpdate}
227
- onDelete={handleDeleteIncident}
228
- onCreate={() => (showIncidentForm = true)}
229
- />
230
- </div>
231
- {:else if activeTab === 'systems'}
232
- <SystemManager
233
- {systems}
234
- onAdd={handleAddSystem}
235
- onUpdateStatus={handleUpdateStatus}
236
- onDelete={handleDeleteSystem}
237
- />
238
- {:else if activeTab === 'incidents'}
239
- {#if showIncidentForm}
240
- <IncidentForm
241
- {systems}
242
- onSave={handleCreateIncident}
243
- onCancel={() => (showIncidentForm = false)}
244
- />
245
- {:else}
246
- <IncidentManager
247
- {incidents}
248
- onAddUpdate={handleAddIncidentUpdate}
249
- onDelete={handleDeleteIncident}
250
- onCreate={() => (showIncidentForm = true)}
251
- />
252
- {/if}
253
- {:else if activeTab === 'feedback'}
254
- <FeedbackList
255
- items={feedbackItems}
256
- total={feedbackTotal}
257
- page={feedbackPage}
258
- onStatusChange={handleFeedbackStatusChange}
259
- onDelete={handleDeleteFeedback}
260
- filterStatus={feedbackFilterStatus || undefined}
261
- onFilterStatus={(s) => {
262
- feedbackFilterStatus = s;
263
- feedbackPage = 1;
264
- loadFeedback();
265
- }}
266
- onPageChange={(p) => {
267
- feedbackPage = p;
268
- loadFeedback();
269
- }}
270
- />
271
- {/if}
272
- {/if}
273
- </div>
274
- </div>
@@ -1,9 +0,0 @@
1
- import type { Project } from '../../types.js';
2
- interface Props {
3
- projects: Project[];
4
- apiEndpoint: string;
5
- onRefresh?: () => Promise<void>;
6
- }
7
- declare const AdminDashboard: import("svelte").Component<Props, {}, "">;
8
- type AdminDashboard = ReturnType<typeof AdminDashboard>;
9
- export default AdminDashboard;
@@ -1,68 +0,0 @@
1
- <script lang="ts">
2
- interface Props {
3
- apiKey: string;
4
- onRegenerate?: () => Promise<void>;
5
- }
6
-
7
- let { apiKey, onRegenerate }: Props = $props();
8
-
9
- let revealed = $state(false);
10
- let copied = $state(false);
11
- let regenerating = $state(false);
12
-
13
- let maskedKey = $derived(
14
- revealed ? apiKey : apiKey.slice(0, 6) + '•'.repeat(Math.max(0, apiKey.length - 10)) + apiKey.slice(-4)
15
- );
16
-
17
- async function copyKey() {
18
- await navigator.clipboard.writeText(apiKey);
19
- copied = true;
20
- setTimeout(() => (copied = false), 2000);
21
- }
22
-
23
- async function handleRegenerate() {
24
- if (!confirm('Regenerate API key? The old key will stop working immediately.')) return;
25
- regenerating = true;
26
- try {
27
- await onRegenerate?.();
28
- } finally {
29
- regenerating = false;
30
- }
31
- }
32
- </script>
33
-
34
- <div class="rounded-lg border border-border bg-surface p-4">
35
- <div class="mb-2 flex items-center gap-2">
36
- <i class="fa-solid fa-key text-sm text-foreground-tertiary"></i>
37
- <span class="text-sm font-semibold text-foreground">API Key</span>
38
- </div>
39
- <div class="flex items-center gap-2">
40
- <code class="flex-1 rounded bg-background px-3 py-2 text-xs text-foreground-secondary">
41
- {maskedKey}
42
- </code>
43
- <button
44
- onclick={() => (revealed = !revealed)}
45
- class="rounded-md border border-border px-2.5 py-2 text-xs text-foreground-secondary transition-colors hover:bg-surface-hover"
46
- aria-label={revealed ? 'Hide API key' : 'Reveal API key'}
47
- >
48
- <i class="fa-solid {revealed ? 'fa-eye-slash' : 'fa-eye'}"></i>
49
- </button>
50
- <button
51
- onclick={copyKey}
52
- class="rounded-md border border-border px-2.5 py-2 text-xs text-foreground-secondary transition-colors hover:bg-surface-hover"
53
- aria-label="Copy API key"
54
- >
55
- <i class="fa-solid {copied ? 'fa-check' : 'fa-copy'}"></i>
56
- </button>
57
- {#if onRegenerate}
58
- <button
59
- onclick={handleRegenerate}
60
- disabled={regenerating}
61
- class="rounded-md border border-border px-2.5 py-2 text-xs text-red-500 transition-colors hover:bg-red-500/10 disabled:opacity-50"
62
- aria-label="Regenerate API key"
63
- >
64
- <i class="fa-solid {regenerating ? 'fa-spinner fa-spin' : 'fa-rotate'}"></i>
65
- </button>
66
- {/if}
67
- </div>
68
- </div>
@@ -1,7 +0,0 @@
1
- interface Props {
2
- apiKey: string;
3
- onRegenerate?: () => Promise<void>;
4
- }
5
- declare const ApiKeyManager: import("svelte").Component<Props, {}, "">;
6
- type ApiKeyManager = ReturnType<typeof ApiKeyManager>;
7
- export default ApiKeyManager;
@@ -1,160 +0,0 @@
1
- <script lang="ts">
2
- import type { Feedback, FeedbackStatus } from '../../types.js';
3
-
4
- interface Props {
5
- items: Feedback[];
6
- total: number;
7
- page?: number;
8
- pageSize?: number;
9
- onStatusChange?: (feedbackId: string, status: FeedbackStatus) => Promise<void>;
10
- onDelete?: (feedbackId: string) => Promise<void>;
11
- onPageChange?: (page: number) => void;
12
- filterCategory?: string;
13
- filterStatus?: FeedbackStatus;
14
- onFilterCategory?: (category: string) => void;
15
- onFilterStatus?: (status: FeedbackStatus | '') => void;
16
- }
17
-
18
- let {
19
- items,
20
- total,
21
- page = 1,
22
- pageSize = 20,
23
- onStatusChange,
24
- onDelete,
25
- onPageChange,
26
- filterCategory,
27
- filterStatus,
28
- onFilterCategory,
29
- onFilterStatus,
30
- }: Props = $props();
31
-
32
- let totalPages = $derived(Math.ceil(total / pageSize));
33
-
34
- const statusOptions: FeedbackStatus[] = ['new', 'reviewed', 'resolved', 'archived'];
35
- const statusColors: Record<FeedbackStatus, string> = {
36
- new: 'text-blue-500 bg-blue-500/10',
37
- reviewed: 'text-yellow-500 bg-yellow-500/10',
38
- resolved: 'text-green-500 bg-green-500/10',
39
- archived: 'text-foreground-tertiary bg-surface-hover',
40
- };
41
-
42
- function formatDate(timestamp: number): string {
43
- return new Date(timestamp).toLocaleString();
44
- }
45
-
46
- function truncate(text: string, max: number): string {
47
- return text.length > max ? text.slice(0, max) + '…' : text;
48
- }
49
- </script>
50
-
51
- <div class="space-y-4">
52
- <div class="flex items-center justify-between">
53
- <h3 class="text-base font-semibold text-foreground">
54
- <i class="fa-solid fa-comment mr-1.5 text-foreground-tertiary"></i>
55
- Feedback
56
- <span class="ml-1 text-sm text-foreground-tertiary">({total})</span>
57
- </h3>
58
- </div>
59
-
60
- <!-- Filters -->
61
- <div class="flex flex-wrap gap-2">
62
- <button
63
- onclick={() => onFilterStatus?.('')}
64
- class="rounded-md px-2.5 py-1 text-xs transition-colors {!filterStatus
65
- ? 'bg-primary text-primary-foreground'
66
- : 'border border-border text-foreground-secondary hover:bg-surface-hover'}"
67
- >
68
- All
69
- </button>
70
- {#each statusOptions as s}
71
- <button
72
- onclick={() => onFilterStatus?.(s)}
73
- class="rounded-md px-2.5 py-1 text-xs capitalize transition-colors {filterStatus === s
74
- ? 'bg-primary text-primary-foreground'
75
- : 'border border-border text-foreground-secondary hover:bg-surface-hover'}"
76
- >
77
- {s}
78
- </button>
79
- {/each}
80
- </div>
81
-
82
- <!-- Items -->
83
- {#if items.length === 0}
84
- <div class="rounded-lg border border-border bg-surface p-6 text-center">
85
- <p class="text-sm text-foreground-secondary">No feedback found</p>
86
- </div>
87
- {:else}
88
- <div class="space-y-2">
89
- {#each items as fb}
90
- <div class="rounded-lg border border-border bg-surface p-3">
91
- <div class="flex items-start justify-between gap-2">
92
- <div class="flex-1">
93
- <div class="flex items-center gap-2 mb-1">
94
- {#if fb.category}
95
- <span class="rounded bg-surface-hover px-1.5 py-0.5 text-xs text-foreground-secondary">
96
- {fb.category}
97
- </span>
98
- {/if}
99
- <span class="rounded px-1.5 py-0.5 text-xs capitalize {statusColors[fb.status]}">
100
- {fb.status}
101
- </span>
102
- <span class="text-xs text-foreground-tertiary">{formatDate(fb.createdAt)}</span>
103
- </div>
104
- <p class="text-sm text-foreground">{truncate(fb.message, 200)}</p>
105
- {#if fb.email}
106
- <p class="mt-1 text-xs text-foreground-tertiary">
107
- <i class="fa-solid fa-envelope mr-1"></i>
108
- {fb.email}
109
- </p>
110
- {/if}
111
- </div>
112
- <div class="flex items-center gap-1">
113
- <select
114
- value={fb.status}
115
- onchange={(e) => onStatusChange?.(fb.id, (e.target as HTMLSelectElement).value as FeedbackStatus)}
116
- class="rounded border border-border bg-background px-2 py-1 text-xs text-foreground"
117
- >
118
- {#each statusOptions as s}
119
- <option value={s}>{s}</option>
120
- {/each}
121
- </select>
122
- <button
123
- onclick={() => onDelete?.(fb.id)}
124
- class="rounded p-1 text-xs text-foreground-tertiary transition-colors hover:text-red-500"
125
- aria-label="Delete feedback"
126
- >
127
- <i class="fa-solid fa-trash"></i>
128
- </button>
129
- </div>
130
- </div>
131
- </div>
132
- {/each}
133
- </div>
134
- {/if}
135
-
136
- <!-- Pagination -->
137
- {#if totalPages > 1}
138
- <div class="flex items-center justify-center gap-2">
139
- <button
140
- onclick={() => onPageChange?.(page - 1)}
141
- disabled={page <= 1}
142
- class="rounded-md border border-border px-3 py-1.5 text-xs text-foreground-secondary disabled:opacity-50"
143
- aria-label="Previous page"
144
- >
145
- <i class="fa-solid fa-chevron-left"></i>
146
- </button>
147
- <span class="text-xs text-foreground-tertiary">
148
- Page {page} of {totalPages}
149
- </span>
150
- <button
151
- onclick={() => onPageChange?.(page + 1)}
152
- disabled={page >= totalPages}
153
- class="rounded-md border border-border px-3 py-1.5 text-xs text-foreground-secondary disabled:opacity-50"
154
- aria-label="Next page"
155
- >
156
- <i class="fa-solid fa-chevron-right"></i>
157
- </button>
158
- </div>
159
- {/if}
160
- </div>
@@ -1,17 +0,0 @@
1
- import type { Feedback, FeedbackStatus } from '../../types.js';
2
- interface Props {
3
- items: Feedback[];
4
- total: number;
5
- page?: number;
6
- pageSize?: number;
7
- onStatusChange?: (feedbackId: string, status: FeedbackStatus) => Promise<void>;
8
- onDelete?: (feedbackId: string) => Promise<void>;
9
- onPageChange?: (page: number) => void;
10
- filterCategory?: string;
11
- filterStatus?: FeedbackStatus;
12
- onFilterCategory?: (category: string) => void;
13
- onFilterStatus?: (status: FeedbackStatus | '') => void;
14
- }
15
- declare const FeedbackList: import("svelte").Component<Props, {}, "">;
16
- type FeedbackList = ReturnType<typeof FeedbackList>;
17
- export default FeedbackList;
@@ -1,135 +0,0 @@
1
- <script lang="ts">
2
- import type { System, IncidentSeverity } from '../../types.js';
3
-
4
- interface Props {
5
- systems: System[];
6
- onSave?: (data: {
7
- title: string;
8
- severity: IncidentSeverity;
9
- affectedSystems: string[];
10
- message: string;
11
- }) => Promise<void>;
12
- onCancel?: () => void;
13
- }
14
-
15
- let { systems, onSave, onCancel }: Props = $props();
16
-
17
- let title = $state('');
18
- let severity = $state<IncidentSeverity>('minor');
19
- let message = $state('');
20
- let selectedSystems = $state<string[]>([]);
21
- let saving = $state(false);
22
-
23
- const severities: IncidentSeverity[] = ['minor', 'major', 'critical'];
24
-
25
- function toggleSystem(id: string) {
26
- if (selectedSystems.includes(id)) {
27
- selectedSystems = selectedSystems.filter((s) => s !== id);
28
- } else {
29
- selectedSystems = [...selectedSystems, id];
30
- }
31
- }
32
-
33
- async function handleSubmit(e: Event) {
34
- e.preventDefault();
35
- if (!title.trim() || !message.trim()) return;
36
- saving = true;
37
- try {
38
- await onSave?.({
39
- title: title.trim(),
40
- severity,
41
- affectedSystems: selectedSystems,
42
- message: message.trim(),
43
- });
44
- } finally {
45
- saving = false;
46
- }
47
- }
48
- </script>
49
-
50
- <form onsubmit={handleSubmit} class="space-y-4">
51
- <h3 class="text-base font-semibold text-foreground">Report Incident</h3>
52
-
53
- <div>
54
- <label for="sf-inc-title" class="mb-1 block text-sm font-semibold text-foreground">
55
- Title <span class="text-red-500">*</span>
56
- </label>
57
- <input
58
- id="sf-inc-title"
59
- type="text"
60
- bind:value={title}
61
- required
62
- placeholder="Brief description of the issue"
63
- class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground"
64
- />
65
- </div>
66
-
67
- <div>
68
- <label class="mb-1 block text-sm font-semibold text-foreground">Severity</label>
69
- <div class="flex gap-2">
70
- {#each severities as s}
71
- <button
72
- type="button"
73
- onclick={() => (severity = s)}
74
- class="rounded-md px-3 py-1.5 text-xs font-semibold capitalize transition-colors {severity === s
75
- ? 'bg-primary text-primary-foreground'
76
- : 'border border-border text-foreground-secondary hover:bg-surface-hover'}"
77
- >
78
- {s}
79
- </button>
80
- {/each}
81
- </div>
82
- </div>
83
-
84
- {#if systems.length > 0}
85
- <div>
86
- <label class="mb-1 block text-sm font-semibold text-foreground">Affected Systems</label>
87
- <div class="flex flex-wrap gap-2">
88
- {#each systems as system}
89
- <button
90
- type="button"
91
- onclick={() => toggleSystem(system.id)}
92
- class="rounded-md px-2.5 py-1 text-xs transition-colors {selectedSystems.includes(system.id)
93
- ? 'bg-red-500/10 border border-red-500/30 text-red-500'
94
- : 'border border-border text-foreground-secondary hover:bg-surface-hover'}"
95
- >
96
- {system.name}
97
- </button>
98
- {/each}
99
- </div>
100
- </div>
101
- {/if}
102
-
103
- <div>
104
- <label for="sf-inc-msg" class="mb-1 block text-sm font-semibold text-foreground">
105
- Initial Update <span class="text-red-500">*</span>
106
- </label>
107
- <textarea
108
- id="sf-inc-msg"
109
- bind:value={message}
110
- rows="3"
111
- required
112
- placeholder="What's happening and what are we doing about it?"
113
- class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground"
114
- ></textarea>
115
- </div>
116
-
117
- <div class="flex gap-2">
118
- <button
119
- type="submit"
120
- disabled={saving || !title.trim() || !message.trim()}
121
- class="rounded-md bg-red-500 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-red-600 disabled:opacity-50"
122
- >
123
- {saving ? 'Creating...' : 'Create Incident'}
124
- </button>
125
- {#if onCancel}
126
- <button
127
- type="button"
128
- onclick={onCancel}
129
- class="rounded-md border border-border px-4 py-2 text-sm text-foreground-secondary transition-colors hover:bg-surface-hover"
130
- >
131
- Cancel
132
- </button>
133
- {/if}
134
- </div>
135
- </form>
@@ -1,14 +0,0 @@
1
- import type { System, IncidentSeverity } from '../../types.js';
2
- interface Props {
3
- systems: System[];
4
- onSave?: (data: {
5
- title: string;
6
- severity: IncidentSeverity;
7
- affectedSystems: string[];
8
- message: string;
9
- }) => Promise<void>;
10
- onCancel?: () => void;
11
- }
12
- declare const IncidentForm: import("svelte").Component<Props, {}, "">;
13
- type IncidentForm = ReturnType<typeof IncidentForm>;
14
- export default IncidentForm;