@alliance-droid/status-feedback-system 2.3.2 → 3.0.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/CHANGELOG.md +78 -67
- package/LICENSE +11 -11
- package/README.md +275 -273
- package/dist/components/DevFeedback.svelte +589 -585
- package/dist/components/DevFeedback.svelte.d.ts +7 -2
- package/dist/components/FeedbackBoard.svelte +110 -110
- package/dist/components/FeedbackButton.svelte +72 -72
- package/dist/components/FeedbackForm.svelte +155 -155
- package/dist/components/StatusBanner.svelte +105 -105
- package/dist/components/StatusPage.svelte +244 -244
- package/dist/components/board/FeedbackDetail.svelte +299 -299
- package/dist/components/board/FeedbackItem.svelte +117 -117
- package/dist/components/board/FeedbackList.svelte +58 -58
- package/dist/components/board/FeedbackSidebar.svelte +60 -60
- package/dist/components/board/FeedbackSubmitForm.svelte +123 -123
- package/dist/components/dev/DevContextMenu.svelte +178 -178
- package/dist/components/dev/DevOnboardingToast.svelte +73 -73
- package/dist/components/dev/environment.d.ts +2 -6
- package/dist/components/dev/environment.js +2 -23
- package/dist/components/shared/IncidentTimeline.svelte +55 -55
- package/dist/components/shared/PasteImageInput.svelte +169 -169
- package/dist/components/shared/StatusIndicator.svelte +50 -50
- package/dist/components/shared/SystemCard.svelte +30 -30
- package/dist/components/shared/UptimeBar.svelte +85 -85
- package/package.json +66 -66
|
@@ -1,244 +1,244 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
import type { System, Incident, StatusHistoryEntry, SystemStatus } from '../types.js';
|
|
4
|
-
import { createStatusClient, type StatusClient } from '../api.js';
|
|
5
|
-
import { deriveOverallStatus } from '../utils.js';
|
|
6
|
-
import StatusIndicator from './shared/StatusIndicator.svelte';
|
|
7
|
-
import SystemCard from './shared/SystemCard.svelte';
|
|
8
|
-
import IncidentTimeline from './shared/IncidentTimeline.svelte';
|
|
9
|
-
|
|
10
|
-
interface Props {
|
|
11
|
-
/** Project display name */
|
|
12
|
-
projectName: string;
|
|
13
|
-
/** API URL for fetching data (self-fetching mode) */
|
|
14
|
-
apiUrl?: string;
|
|
15
|
-
/** API key for fetching data (self-fetching mode) */
|
|
16
|
-
apiKey?: string;
|
|
17
|
-
/** Systems data (pass-through mode — overrides API fetching) */
|
|
18
|
-
systems?: System[];
|
|
19
|
-
/** Incidents data (pass-through mode) */
|
|
20
|
-
incidents?: Incident[];
|
|
21
|
-
/** Map of systemId → StatusHistoryEntry[] for uptime bars (pass-through mode) */
|
|
22
|
-
statusHistory?: Record<string, StatusHistoryEntry[]>;
|
|
23
|
-
showUptime?: boolean;
|
|
24
|
-
/** Auto-refresh interval in seconds. 0 = disabled. Default: 0 */
|
|
25
|
-
refreshInterval?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let {
|
|
29
|
-
projectName,
|
|
30
|
-
apiUrl,
|
|
31
|
-
apiKey,
|
|
32
|
-
systems: systemsProp,
|
|
33
|
-
incidents: incidentsProp,
|
|
34
|
-
statusHistory: statusHistoryProp,
|
|
35
|
-
showUptime = true,
|
|
36
|
-
refreshInterval = 0,
|
|
37
|
-
}: Props = $props();
|
|
38
|
-
|
|
39
|
-
// ── State ──
|
|
40
|
-
let systems = $derived(systemsProp ?? []);
|
|
41
|
-
let incidents = $derived(incidentsProp ?? []);
|
|
42
|
-
let statusHistory = $derived(statusHistoryProp ?? {});
|
|
43
|
-
let loading = $derived(!systemsProp);
|
|
44
|
-
let error = $state('');
|
|
45
|
-
|
|
46
|
-
// ── Self-fetching mode ──
|
|
47
|
-
const usesApi = $derived(!!apiUrl && !!apiKey && !systemsProp);
|
|
48
|
-
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
49
|
-
|
|
50
|
-
let cachedClient: StatusClient | null = null;
|
|
51
|
-
let cachedApiUrl = '';
|
|
52
|
-
let cachedApiKey = '';
|
|
53
|
-
|
|
54
|
-
function getClient(): StatusClient {
|
|
55
|
-
if (!cachedClient || cachedApiUrl !== apiUrl || cachedApiKey !== apiKey) {
|
|
56
|
-
cachedClient = createStatusClient({ apiUrl: apiUrl!, apiKey: apiKey! });
|
|
57
|
-
cachedApiUrl = apiUrl!;
|
|
58
|
-
cachedApiKey = apiKey!;
|
|
59
|
-
}
|
|
60
|
-
return cachedClient;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function fetchData() {
|
|
64
|
-
if (!usesApi) return;
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const client = getClient();
|
|
68
|
-
|
|
69
|
-
const [statusData, incidentData, uptimeData] = await Promise.all([
|
|
70
|
-
client.getStatus(),
|
|
71
|
-
client.getIncidents(),
|
|
72
|
-
client.getUptime(),
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
systems = statusData.systems;
|
|
76
|
-
incidents = incidentData.incidents;
|
|
77
|
-
statusHistory = uptimeData.history;
|
|
78
|
-
error = '';
|
|
79
|
-
} catch (err) {
|
|
80
|
-
error = err instanceof Error ? err.message : 'Failed to load status data';
|
|
81
|
-
} finally {
|
|
82
|
-
loading = false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
onMount(() => {
|
|
87
|
-
fetchData();
|
|
88
|
-
|
|
89
|
-
if (refreshInterval > 0) {
|
|
90
|
-
refreshTimer = setInterval(fetchData, refreshInterval * 1000);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return () => {
|
|
94
|
-
if (refreshTimer) clearInterval(refreshTimer);
|
|
95
|
-
};
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ── Derived state ──
|
|
99
|
-
|
|
100
|
-
let overallStatus = $derived(deriveOverallStatus(systems));
|
|
101
|
-
|
|
102
|
-
const overallMessages: Record<SystemStatus, string> = {
|
|
103
|
-
operational: 'All Systems Operational',
|
|
104
|
-
degraded: 'Partial System Degradation',
|
|
105
|
-
outage: 'Partial System Outage',
|
|
106
|
-
maintenance: 'Scheduled Maintenance',
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const overallBgClasses: Record<SystemStatus, string> = {
|
|
110
|
-
operational: 'bg-green-500/10 border-green-500/30',
|
|
111
|
-
degraded: 'bg-yellow-500/10 border-yellow-500/30',
|
|
112
|
-
outage: 'bg-red-500/10 border-red-500/30',
|
|
113
|
-
maintenance: 'bg-blue-500/10 border-blue-500/30',
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
let activeIncidents = $derived(incidents.filter((i) => i.status !== 'resolved'));
|
|
117
|
-
let recentResolved = $derived(
|
|
118
|
-
incidents.filter((i) => i.status === 'resolved').slice(0, 10)
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
function formatDate(timestamp: number): string {
|
|
122
|
-
return new Date(timestamp).toLocaleDateString(undefined, {
|
|
123
|
-
year: 'numeric',
|
|
124
|
-
month: 'long',
|
|
125
|
-
day: 'numeric',
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
</script>
|
|
129
|
-
|
|
130
|
-
<div class="mx-auto max-w-3xl space-y-8">
|
|
131
|
-
<!-- Header -->
|
|
132
|
-
<div class="text-center">
|
|
133
|
-
<h1 class="text-2xl font-semibold text-foreground">{projectName} Status</h1>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
{#if loading}
|
|
137
|
-
<div class="rounded-lg border border-border bg-surface p-12 text-center">
|
|
138
|
-
<div class="mx-auto mb-3 h-8 w-8 animate-spin rounded-full border-2 border-foreground-tertiary border-t-primary"></div>
|
|
139
|
-
<p class="text-sm text-foreground-secondary">Loading status...</p>
|
|
140
|
-
</div>
|
|
141
|
-
{:else if error}
|
|
142
|
-
<div class="rounded-lg border border-red-500/30 bg-red-500/10 p-6 text-center">
|
|
143
|
-
<i class="fa-solid fa-circle-exclamation mb-2 text-2xl text-red-500"></i>
|
|
144
|
-
<p class="text-sm text-red-400">{error}</p>
|
|
145
|
-
{#if usesApi}
|
|
146
|
-
<button
|
|
147
|
-
onclick={() => { loading = true; fetchData(); }}
|
|
148
|
-
class="mt-3 rounded-md bg-surface-hover px-4 py-2 text-sm text-foreground-secondary transition-colors hover:bg-border"
|
|
149
|
-
>
|
|
150
|
-
<i class="fa-solid fa-rotate-right mr-1"></i> Retry
|
|
151
|
-
</button>
|
|
152
|
-
{/if}
|
|
153
|
-
</div>
|
|
154
|
-
{:else}
|
|
155
|
-
<!-- Overall Status Banner -->
|
|
156
|
-
<div class="rounded-lg border p-4 text-center {overallBgClasses[overallStatus]}">
|
|
157
|
-
<div class="flex items-center justify-center gap-2">
|
|
158
|
-
<StatusIndicator status={overallStatus} size="lg" />
|
|
159
|
-
<span class="text-lg font-semibold text-foreground">{overallMessages[overallStatus]}</span>
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
<!-- Systems -->
|
|
164
|
-
{#if systems.length > 0}
|
|
165
|
-
<section>
|
|
166
|
-
<h2 class="mb-4 text-lg font-semibold text-foreground">Systems</h2>
|
|
167
|
-
<div class="space-y-3">
|
|
168
|
-
{#each systems as system (system.id)}
|
|
169
|
-
<SystemCard
|
|
170
|
-
{system}
|
|
171
|
-
history={statusHistory[system.id] || []}
|
|
172
|
-
{showUptime}
|
|
173
|
-
/>
|
|
174
|
-
{/each}
|
|
175
|
-
</div>
|
|
176
|
-
</section>
|
|
177
|
-
{/if}
|
|
178
|
-
|
|
179
|
-
<!-- Active Incidents -->
|
|
180
|
-
{#if activeIncidents.length > 0}
|
|
181
|
-
<section>
|
|
182
|
-
<h2 class="mb-4 text-lg font-semibold text-foreground">Active Incidents</h2>
|
|
183
|
-
<div class="space-y-4">
|
|
184
|
-
{#each activeIncidents as incident (incident.id)}
|
|
185
|
-
<div class="rounded-lg border border-border bg-surface p-4">
|
|
186
|
-
<div class="flex items-start justify-between">
|
|
187
|
-
<div>
|
|
188
|
-
<h3 class="font-semibold text-foreground">{incident.title}</h3>
|
|
189
|
-
<div class="mt-1 flex items-center gap-2 text-xs text-foreground-tertiary">
|
|
190
|
-
<span class="rounded bg-red-500/10 px-1.5 py-0.5 text-red-500">
|
|
191
|
-
{incident.severity}
|
|
192
|
-
</span>
|
|
193
|
-
<span>{formatDate(incident.createdAt)}</span>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
<StatusIndicator status={incident.status === 'resolved' ? 'operational' : 'outage'} size="sm" />
|
|
197
|
-
</div>
|
|
198
|
-
{#if incident.updates && incident.updates.length > 0}
|
|
199
|
-
<div class="mt-4 border-t border-border pt-4">
|
|
200
|
-
<IncidentTimeline updates={incident.updates} />
|
|
201
|
-
</div>
|
|
202
|
-
{/if}
|
|
203
|
-
</div>
|
|
204
|
-
{/each}
|
|
205
|
-
</div>
|
|
206
|
-
</section>
|
|
207
|
-
{/if}
|
|
208
|
-
|
|
209
|
-
<!-- Resolved Incidents -->
|
|
210
|
-
{#if recentResolved.length > 0}
|
|
211
|
-
<section>
|
|
212
|
-
<h2 class="mb-4 text-lg font-semibold text-foreground">Past Incidents</h2>
|
|
213
|
-
<div class="space-y-4">
|
|
214
|
-
{#each recentResolved as incident (incident.id)}
|
|
215
|
-
<div class="rounded-lg border border-border bg-surface p-4">
|
|
216
|
-
<div class="flex items-start justify-between">
|
|
217
|
-
<div>
|
|
218
|
-
<h3 class="font-semibold text-foreground">{incident.title}</h3>
|
|
219
|
-
<p class="mt-1 text-xs text-foreground-tertiary">
|
|
220
|
-
Resolved {incident.resolvedAt ? formatDate(incident.resolvedAt) : ''}
|
|
221
|
-
</p>
|
|
222
|
-
</div>
|
|
223
|
-
<StatusIndicator status="operational" size="sm" />
|
|
224
|
-
</div>
|
|
225
|
-
{#if incident.updates && incident.updates.length > 0}
|
|
226
|
-
<div class="mt-4 border-t border-border pt-4">
|
|
227
|
-
<IncidentTimeline updates={incident.updates} />
|
|
228
|
-
</div>
|
|
229
|
-
{/if}
|
|
230
|
-
</div>
|
|
231
|
-
{/each}
|
|
232
|
-
</div>
|
|
233
|
-
</section>
|
|
234
|
-
{/if}
|
|
235
|
-
|
|
236
|
-
<!-- No Incidents -->
|
|
237
|
-
{#if activeIncidents.length === 0 && recentResolved.length === 0}
|
|
238
|
-
<div class="rounded-lg border border-border bg-surface p-8 text-center">
|
|
239
|
-
<i class="fa-solid fa-circle-check mb-2 text-2xl text-green-500"></i>
|
|
240
|
-
<p class="text-sm text-foreground-secondary">No incidents reported</p>
|
|
241
|
-
</div>
|
|
242
|
-
{/if}
|
|
243
|
-
{/if}
|
|
244
|
-
</div>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { System, Incident, StatusHistoryEntry, SystemStatus } from '../types.js';
|
|
4
|
+
import { createStatusClient, type StatusClient } from '../api.js';
|
|
5
|
+
import { deriveOverallStatus } from '../utils.js';
|
|
6
|
+
import StatusIndicator from './shared/StatusIndicator.svelte';
|
|
7
|
+
import SystemCard from './shared/SystemCard.svelte';
|
|
8
|
+
import IncidentTimeline from './shared/IncidentTimeline.svelte';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Project display name */
|
|
12
|
+
projectName: string;
|
|
13
|
+
/** API URL for fetching data (self-fetching mode) */
|
|
14
|
+
apiUrl?: string;
|
|
15
|
+
/** API key for fetching data (self-fetching mode) */
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
/** Systems data (pass-through mode — overrides API fetching) */
|
|
18
|
+
systems?: System[];
|
|
19
|
+
/** Incidents data (pass-through mode) */
|
|
20
|
+
incidents?: Incident[];
|
|
21
|
+
/** Map of systemId → StatusHistoryEntry[] for uptime bars (pass-through mode) */
|
|
22
|
+
statusHistory?: Record<string, StatusHistoryEntry[]>;
|
|
23
|
+
showUptime?: boolean;
|
|
24
|
+
/** Auto-refresh interval in seconds. 0 = disabled. Default: 0 */
|
|
25
|
+
refreshInterval?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
projectName,
|
|
30
|
+
apiUrl,
|
|
31
|
+
apiKey,
|
|
32
|
+
systems: systemsProp,
|
|
33
|
+
incidents: incidentsProp,
|
|
34
|
+
statusHistory: statusHistoryProp,
|
|
35
|
+
showUptime = true,
|
|
36
|
+
refreshInterval = 0,
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
// ── State ──
|
|
40
|
+
let systems = $derived(systemsProp ?? []);
|
|
41
|
+
let incidents = $derived(incidentsProp ?? []);
|
|
42
|
+
let statusHistory = $derived(statusHistoryProp ?? {});
|
|
43
|
+
let loading = $derived(!systemsProp);
|
|
44
|
+
let error = $state('');
|
|
45
|
+
|
|
46
|
+
// ── Self-fetching mode ──
|
|
47
|
+
const usesApi = $derived(!!apiUrl && !!apiKey && !systemsProp);
|
|
48
|
+
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
49
|
+
|
|
50
|
+
let cachedClient: StatusClient | null = null;
|
|
51
|
+
let cachedApiUrl = '';
|
|
52
|
+
let cachedApiKey = '';
|
|
53
|
+
|
|
54
|
+
function getClient(): StatusClient {
|
|
55
|
+
if (!cachedClient || cachedApiUrl !== apiUrl || cachedApiKey !== apiKey) {
|
|
56
|
+
cachedClient = createStatusClient({ apiUrl: apiUrl!, apiKey: apiKey! });
|
|
57
|
+
cachedApiUrl = apiUrl!;
|
|
58
|
+
cachedApiKey = apiKey!;
|
|
59
|
+
}
|
|
60
|
+
return cachedClient;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function fetchData() {
|
|
64
|
+
if (!usesApi) return;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const client = getClient();
|
|
68
|
+
|
|
69
|
+
const [statusData, incidentData, uptimeData] = await Promise.all([
|
|
70
|
+
client.getStatus(),
|
|
71
|
+
client.getIncidents(),
|
|
72
|
+
client.getUptime(),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
systems = statusData.systems;
|
|
76
|
+
incidents = incidentData.incidents;
|
|
77
|
+
statusHistory = uptimeData.history;
|
|
78
|
+
error = '';
|
|
79
|
+
} catch (err) {
|
|
80
|
+
error = err instanceof Error ? err.message : 'Failed to load status data';
|
|
81
|
+
} finally {
|
|
82
|
+
loading = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onMount(() => {
|
|
87
|
+
fetchData();
|
|
88
|
+
|
|
89
|
+
if (refreshInterval > 0) {
|
|
90
|
+
refreshTimer = setInterval(fetchData, refreshInterval * 1000);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Derived state ──
|
|
99
|
+
|
|
100
|
+
let overallStatus = $derived(deriveOverallStatus(systems));
|
|
101
|
+
|
|
102
|
+
const overallMessages: Record<SystemStatus, string> = {
|
|
103
|
+
operational: 'All Systems Operational',
|
|
104
|
+
degraded: 'Partial System Degradation',
|
|
105
|
+
outage: 'Partial System Outage',
|
|
106
|
+
maintenance: 'Scheduled Maintenance',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const overallBgClasses: Record<SystemStatus, string> = {
|
|
110
|
+
operational: 'bg-green-500/10 border-green-500/30',
|
|
111
|
+
degraded: 'bg-yellow-500/10 border-yellow-500/30',
|
|
112
|
+
outage: 'bg-red-500/10 border-red-500/30',
|
|
113
|
+
maintenance: 'bg-blue-500/10 border-blue-500/30',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let activeIncidents = $derived(incidents.filter((i) => i.status !== 'resolved'));
|
|
117
|
+
let recentResolved = $derived(
|
|
118
|
+
incidents.filter((i) => i.status === 'resolved').slice(0, 10)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
function formatDate(timestamp: number): string {
|
|
122
|
+
return new Date(timestamp).toLocaleDateString(undefined, {
|
|
123
|
+
year: 'numeric',
|
|
124
|
+
month: 'long',
|
|
125
|
+
day: 'numeric',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<div class="mx-auto max-w-3xl space-y-8">
|
|
131
|
+
<!-- Header -->
|
|
132
|
+
<div class="text-center">
|
|
133
|
+
<h1 class="text-2xl font-semibold text-foreground">{projectName} Status</h1>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{#if loading}
|
|
137
|
+
<div class="rounded-lg border border-border bg-surface p-12 text-center">
|
|
138
|
+
<div class="mx-auto mb-3 h-8 w-8 animate-spin rounded-full border-2 border-foreground-tertiary border-t-primary"></div>
|
|
139
|
+
<p class="text-sm text-foreground-secondary">Loading status...</p>
|
|
140
|
+
</div>
|
|
141
|
+
{:else if error}
|
|
142
|
+
<div class="rounded-lg border border-red-500/30 bg-red-500/10 p-6 text-center">
|
|
143
|
+
<i class="fa-solid fa-circle-exclamation mb-2 text-2xl text-red-500"></i>
|
|
144
|
+
<p class="text-sm text-red-400">{error}</p>
|
|
145
|
+
{#if usesApi}
|
|
146
|
+
<button
|
|
147
|
+
onclick={() => { loading = true; fetchData(); }}
|
|
148
|
+
class="mt-3 rounded-md bg-surface-hover px-4 py-2 text-sm text-foreground-secondary transition-colors hover:bg-border"
|
|
149
|
+
>
|
|
150
|
+
<i class="fa-solid fa-rotate-right mr-1"></i> Retry
|
|
151
|
+
</button>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
{:else}
|
|
155
|
+
<!-- Overall Status Banner -->
|
|
156
|
+
<div class="rounded-lg border p-4 text-center {overallBgClasses[overallStatus]}">
|
|
157
|
+
<div class="flex items-center justify-center gap-2">
|
|
158
|
+
<StatusIndicator status={overallStatus} size="lg" />
|
|
159
|
+
<span class="text-lg font-semibold text-foreground">{overallMessages[overallStatus]}</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Systems -->
|
|
164
|
+
{#if systems.length > 0}
|
|
165
|
+
<section>
|
|
166
|
+
<h2 class="mb-4 text-lg font-semibold text-foreground">Systems</h2>
|
|
167
|
+
<div class="space-y-3">
|
|
168
|
+
{#each systems as system (system.id)}
|
|
169
|
+
<SystemCard
|
|
170
|
+
{system}
|
|
171
|
+
history={statusHistory[system.id] || []}
|
|
172
|
+
{showUptime}
|
|
173
|
+
/>
|
|
174
|
+
{/each}
|
|
175
|
+
</div>
|
|
176
|
+
</section>
|
|
177
|
+
{/if}
|
|
178
|
+
|
|
179
|
+
<!-- Active Incidents -->
|
|
180
|
+
{#if activeIncidents.length > 0}
|
|
181
|
+
<section>
|
|
182
|
+
<h2 class="mb-4 text-lg font-semibold text-foreground">Active Incidents</h2>
|
|
183
|
+
<div class="space-y-4">
|
|
184
|
+
{#each activeIncidents as incident (incident.id)}
|
|
185
|
+
<div class="rounded-lg border border-border bg-surface p-4">
|
|
186
|
+
<div class="flex items-start justify-between">
|
|
187
|
+
<div>
|
|
188
|
+
<h3 class="font-semibold text-foreground">{incident.title}</h3>
|
|
189
|
+
<div class="mt-1 flex items-center gap-2 text-xs text-foreground-tertiary">
|
|
190
|
+
<span class="rounded bg-red-500/10 px-1.5 py-0.5 text-red-500">
|
|
191
|
+
{incident.severity}
|
|
192
|
+
</span>
|
|
193
|
+
<span>{formatDate(incident.createdAt)}</span>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<StatusIndicator status={incident.status === 'resolved' ? 'operational' : 'outage'} size="sm" />
|
|
197
|
+
</div>
|
|
198
|
+
{#if incident.updates && incident.updates.length > 0}
|
|
199
|
+
<div class="mt-4 border-t border-border pt-4">
|
|
200
|
+
<IncidentTimeline updates={incident.updates} />
|
|
201
|
+
</div>
|
|
202
|
+
{/if}
|
|
203
|
+
</div>
|
|
204
|
+
{/each}
|
|
205
|
+
</div>
|
|
206
|
+
</section>
|
|
207
|
+
{/if}
|
|
208
|
+
|
|
209
|
+
<!-- Resolved Incidents -->
|
|
210
|
+
{#if recentResolved.length > 0}
|
|
211
|
+
<section>
|
|
212
|
+
<h2 class="mb-4 text-lg font-semibold text-foreground">Past Incidents</h2>
|
|
213
|
+
<div class="space-y-4">
|
|
214
|
+
{#each recentResolved as incident (incident.id)}
|
|
215
|
+
<div class="rounded-lg border border-border bg-surface p-4">
|
|
216
|
+
<div class="flex items-start justify-between">
|
|
217
|
+
<div>
|
|
218
|
+
<h3 class="font-semibold text-foreground">{incident.title}</h3>
|
|
219
|
+
<p class="mt-1 text-xs text-foreground-tertiary">
|
|
220
|
+
Resolved {incident.resolvedAt ? formatDate(incident.resolvedAt) : ''}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
<StatusIndicator status="operational" size="sm" />
|
|
224
|
+
</div>
|
|
225
|
+
{#if incident.updates && incident.updates.length > 0}
|
|
226
|
+
<div class="mt-4 border-t border-border pt-4">
|
|
227
|
+
<IncidentTimeline updates={incident.updates} />
|
|
228
|
+
</div>
|
|
229
|
+
{/if}
|
|
230
|
+
</div>
|
|
231
|
+
{/each}
|
|
232
|
+
</div>
|
|
233
|
+
</section>
|
|
234
|
+
{/if}
|
|
235
|
+
|
|
236
|
+
<!-- No Incidents -->
|
|
237
|
+
{#if activeIncidents.length === 0 && recentResolved.length === 0}
|
|
238
|
+
<div class="rounded-lg border border-border bg-surface p-8 text-center">
|
|
239
|
+
<i class="fa-solid fa-circle-check mb-2 text-2xl text-green-500"></i>
|
|
240
|
+
<p class="text-sm text-foreground-secondary">No incidents reported</p>
|
|
241
|
+
</div>
|
|
242
|
+
{/if}
|
|
243
|
+
{/if}
|
|
244
|
+
</div>
|