@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,155 +1,155 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
3
|
-
import { createStatusClient, type StatusClient } from '../api.js';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
apiKey: string;
|
|
7
|
-
apiUrl: string;
|
|
8
|
-
categories?: string[];
|
|
9
|
-
userEmail?: string;
|
|
10
|
-
onSuccess?: () => void;
|
|
11
|
-
onError?: (error: string) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let {
|
|
15
|
-
apiKey,
|
|
16
|
-
apiUrl,
|
|
17
|
-
categories = ['Bug', 'Feature Request', 'General'],
|
|
18
|
-
userEmail,
|
|
19
|
-
onSuccess,
|
|
20
|
-
onError,
|
|
21
|
-
}: Props = $props();
|
|
22
|
-
|
|
23
|
-
let client: StatusClient | null = $state(null);
|
|
24
|
-
let lastApiUrl = '';
|
|
25
|
-
let lastApiKey = '';
|
|
26
|
-
|
|
27
|
-
function getClient(): StatusClient {
|
|
28
|
-
if (!client || lastApiUrl !== apiUrl || lastApiKey !== apiKey) {
|
|
29
|
-
client = createStatusClient({ apiUrl, apiKey });
|
|
30
|
-
lastApiUrl = apiUrl;
|
|
31
|
-
lastApiKey = apiKey;
|
|
32
|
-
}
|
|
33
|
-
return client;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let category = $state(untrack(() => categories[0] || ''));
|
|
37
|
-
let message = $state('');
|
|
38
|
-
let email = $state(untrack(() => userEmail ?? ''));
|
|
39
|
-
let submitting = $state(false);
|
|
40
|
-
let submitted = $state(false);
|
|
41
|
-
let errorMsg = $state('');
|
|
42
|
-
|
|
43
|
-
async function handleSubmit(e: Event) {
|
|
44
|
-
e.preventDefault();
|
|
45
|
-
if (!message.trim()) return;
|
|
46
|
-
|
|
47
|
-
submitting = true;
|
|
48
|
-
errorMsg = '';
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
await getClient().submitFeedback({
|
|
52
|
-
category,
|
|
53
|
-
message: message.trim(),
|
|
54
|
-
email: email.trim() || undefined,
|
|
55
|
-
pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
submitted = true;
|
|
59
|
-
onSuccess?.();
|
|
60
|
-
} catch (err) {
|
|
61
|
-
errorMsg = err instanceof Error ? err.message : 'Something went wrong';
|
|
62
|
-
onError?.(errorMsg);
|
|
63
|
-
} finally {
|
|
64
|
-
submitting = false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function reset() {
|
|
69
|
-
message = '';
|
|
70
|
-
submitted = false;
|
|
71
|
-
errorMsg = '';
|
|
72
|
-
}
|
|
73
|
-
</script>
|
|
74
|
-
|
|
75
|
-
{#if submitted}
|
|
76
|
-
<div class="rounded-lg border border-green-500/30 bg-green-500/10 p-6 text-center">
|
|
77
|
-
<i class="fa-solid fa-circle-check mb-2 text-2xl text-green-500"></i>
|
|
78
|
-
<h3 class="font-semibold text-foreground">Thanks for your feedback!</h3>
|
|
79
|
-
<p class="mt-1 text-sm text-foreground-secondary">We'll review it shortly.</p>
|
|
80
|
-
<button
|
|
81
|
-
onclick={reset}
|
|
82
|
-
class="mt-4 rounded-md bg-surface-hover px-4 py-2 text-sm text-foreground-secondary transition-colors hover:bg-border"
|
|
83
|
-
>
|
|
84
|
-
Send more feedback
|
|
85
|
-
</button>
|
|
86
|
-
</div>
|
|
87
|
-
{:else}
|
|
88
|
-
<form onsubmit={handleSubmit} class="space-y-4">
|
|
89
|
-
{#if categories.length > 1}
|
|
90
|
-
<div>
|
|
91
|
-
<label for="sf-category" class="mb-1 block text-sm font-semibold text-foreground">
|
|
92
|
-
Category
|
|
93
|
-
</label>
|
|
94
|
-
<select
|
|
95
|
-
id="sf-category"
|
|
96
|
-
bind:value={category}
|
|
97
|
-
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground"
|
|
98
|
-
>
|
|
99
|
-
{#each categories as cat (cat)}
|
|
100
|
-
<option value={cat}>{cat}</option>
|
|
101
|
-
{/each}
|
|
102
|
-
</select>
|
|
103
|
-
</div>
|
|
104
|
-
{/if}
|
|
105
|
-
|
|
106
|
-
<div>
|
|
107
|
-
<label for="sf-message" class="mb-1 block text-sm font-semibold text-foreground">
|
|
108
|
-
Message <span class="text-red-500">*</span>
|
|
109
|
-
</label>
|
|
110
|
-
<textarea
|
|
111
|
-
id="sf-message"
|
|
112
|
-
bind:value={message}
|
|
113
|
-
placeholder="Tell us what's on your mind..."
|
|
114
|
-
rows="4"
|
|
115
|
-
maxlength="5000"
|
|
116
|
-
required
|
|
117
|
-
class="w-full resize-y rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground placeholder:text-foreground-tertiary"
|
|
118
|
-
></textarea>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<div>
|
|
122
|
-
<label for="sf-email" class="mb-1 block text-sm font-semibold text-foreground">
|
|
123
|
-
Email <span class="text-foreground-tertiary">(optional)</span>
|
|
124
|
-
</label>
|
|
125
|
-
<input
|
|
126
|
-
id="sf-email"
|
|
127
|
-
type="email"
|
|
128
|
-
bind:value={email}
|
|
129
|
-
placeholder="your@email.com"
|
|
130
|
-
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground placeholder:text-foreground-tertiary"
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
{#if errorMsg}
|
|
135
|
-
<div class="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-500">
|
|
136
|
-
<i class="fa-solid fa-circle-exclamation mr-1"></i>
|
|
137
|
-
{errorMsg}
|
|
138
|
-
</div>
|
|
139
|
-
{/if}
|
|
140
|
-
|
|
141
|
-
<button
|
|
142
|
-
type="submit"
|
|
143
|
-
disabled={submitting || !message.trim()}
|
|
144
|
-
class="w-full rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
|
145
|
-
>
|
|
146
|
-
{#if submitting}
|
|
147
|
-
<i class="fa-solid fa-spinner fa-spin mr-1"></i>
|
|
148
|
-
Submitting...
|
|
149
|
-
{:else}
|
|
150
|
-
<i class="fa-solid fa-paper-plane mr-1"></i>
|
|
151
|
-
Submit Feedback
|
|
152
|
-
{/if}
|
|
153
|
-
</button>
|
|
154
|
-
</form>
|
|
155
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { createStatusClient, type StatusClient } from '../api.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
categories?: string[];
|
|
9
|
+
userEmail?: string;
|
|
10
|
+
onSuccess?: () => void;
|
|
11
|
+
onError?: (error: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
apiKey,
|
|
16
|
+
apiUrl,
|
|
17
|
+
categories = ['Bug', 'Feature Request', 'General'],
|
|
18
|
+
userEmail,
|
|
19
|
+
onSuccess,
|
|
20
|
+
onError,
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
|
|
23
|
+
let client: StatusClient | null = $state(null);
|
|
24
|
+
let lastApiUrl = '';
|
|
25
|
+
let lastApiKey = '';
|
|
26
|
+
|
|
27
|
+
function getClient(): StatusClient {
|
|
28
|
+
if (!client || lastApiUrl !== apiUrl || lastApiKey !== apiKey) {
|
|
29
|
+
client = createStatusClient({ apiUrl, apiKey });
|
|
30
|
+
lastApiUrl = apiUrl;
|
|
31
|
+
lastApiKey = apiKey;
|
|
32
|
+
}
|
|
33
|
+
return client;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let category = $state(untrack(() => categories[0] || ''));
|
|
37
|
+
let message = $state('');
|
|
38
|
+
let email = $state(untrack(() => userEmail ?? ''));
|
|
39
|
+
let submitting = $state(false);
|
|
40
|
+
let submitted = $state(false);
|
|
41
|
+
let errorMsg = $state('');
|
|
42
|
+
|
|
43
|
+
async function handleSubmit(e: Event) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
if (!message.trim()) return;
|
|
46
|
+
|
|
47
|
+
submitting = true;
|
|
48
|
+
errorMsg = '';
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await getClient().submitFeedback({
|
|
52
|
+
category,
|
|
53
|
+
message: message.trim(),
|
|
54
|
+
email: email.trim() || undefined,
|
|
55
|
+
pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
submitted = true;
|
|
59
|
+
onSuccess?.();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
errorMsg = err instanceof Error ? err.message : 'Something went wrong';
|
|
62
|
+
onError?.(errorMsg);
|
|
63
|
+
} finally {
|
|
64
|
+
submitting = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function reset() {
|
|
69
|
+
message = '';
|
|
70
|
+
submitted = false;
|
|
71
|
+
errorMsg = '';
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
{#if submitted}
|
|
76
|
+
<div class="rounded-lg border border-green-500/30 bg-green-500/10 p-6 text-center">
|
|
77
|
+
<i class="fa-solid fa-circle-check mb-2 text-2xl text-green-500"></i>
|
|
78
|
+
<h3 class="font-semibold text-foreground">Thanks for your feedback!</h3>
|
|
79
|
+
<p class="mt-1 text-sm text-foreground-secondary">We'll review it shortly.</p>
|
|
80
|
+
<button
|
|
81
|
+
onclick={reset}
|
|
82
|
+
class="mt-4 rounded-md bg-surface-hover px-4 py-2 text-sm text-foreground-secondary transition-colors hover:bg-border"
|
|
83
|
+
>
|
|
84
|
+
Send more feedback
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
{:else}
|
|
88
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
89
|
+
{#if categories.length > 1}
|
|
90
|
+
<div>
|
|
91
|
+
<label for="sf-category" class="mb-1 block text-sm font-semibold text-foreground">
|
|
92
|
+
Category
|
|
93
|
+
</label>
|
|
94
|
+
<select
|
|
95
|
+
id="sf-category"
|
|
96
|
+
bind:value={category}
|
|
97
|
+
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground"
|
|
98
|
+
>
|
|
99
|
+
{#each categories as cat (cat)}
|
|
100
|
+
<option value={cat}>{cat}</option>
|
|
101
|
+
{/each}
|
|
102
|
+
</select>
|
|
103
|
+
</div>
|
|
104
|
+
{/if}
|
|
105
|
+
|
|
106
|
+
<div>
|
|
107
|
+
<label for="sf-message" class="mb-1 block text-sm font-semibold text-foreground">
|
|
108
|
+
Message <span class="text-red-500">*</span>
|
|
109
|
+
</label>
|
|
110
|
+
<textarea
|
|
111
|
+
id="sf-message"
|
|
112
|
+
bind:value={message}
|
|
113
|
+
placeholder="Tell us what's on your mind..."
|
|
114
|
+
rows="4"
|
|
115
|
+
maxlength="5000"
|
|
116
|
+
required
|
|
117
|
+
class="w-full resize-y rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground placeholder:text-foreground-tertiary"
|
|
118
|
+
></textarea>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div>
|
|
122
|
+
<label for="sf-email" class="mb-1 block text-sm font-semibold text-foreground">
|
|
123
|
+
Email <span class="text-foreground-tertiary">(optional)</span>
|
|
124
|
+
</label>
|
|
125
|
+
<input
|
|
126
|
+
id="sf-email"
|
|
127
|
+
type="email"
|
|
128
|
+
bind:value={email}
|
|
129
|
+
placeholder="your@email.com"
|
|
130
|
+
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground placeholder:text-foreground-tertiary"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{#if errorMsg}
|
|
135
|
+
<div class="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-500">
|
|
136
|
+
<i class="fa-solid fa-circle-exclamation mr-1"></i>
|
|
137
|
+
{errorMsg}
|
|
138
|
+
</div>
|
|
139
|
+
{/if}
|
|
140
|
+
|
|
141
|
+
<button
|
|
142
|
+
type="submit"
|
|
143
|
+
disabled={submitting || !message.trim()}
|
|
144
|
+
class="w-full rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
|
145
|
+
>
|
|
146
|
+
{#if submitting}
|
|
147
|
+
<i class="fa-solid fa-spinner fa-spin mr-1"></i>
|
|
148
|
+
Submitting...
|
|
149
|
+
{:else}
|
|
150
|
+
<i class="fa-solid fa-paper-plane mr-1"></i>
|
|
151
|
+
Submit Feedback
|
|
152
|
+
{/if}
|
|
153
|
+
</button>
|
|
154
|
+
</form>
|
|
155
|
+
{/if}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
import type { System, 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
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
/** Systems data (pass-through mode) */
|
|
10
|
-
systems?: System[];
|
|
11
|
-
/** API URL for self-fetching mode */
|
|
12
|
-
apiUrl?: string;
|
|
13
|
-
/** API key for self-fetching mode */
|
|
14
|
-
apiKey?: string;
|
|
15
|
-
/** Link to full status page */
|
|
16
|
-
statusUrl?: string;
|
|
17
|
-
/** Auto-refresh interval in seconds. 0 = disabled. Default: 60 */
|
|
18
|
-
refreshInterval?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let { systems: systemsProp, apiUrl, apiKey, statusUrl, refreshInterval = 60 }: Props = $props();
|
|
22
|
-
|
|
23
|
-
let systems = $derived(systemsProp ?? []);
|
|
24
|
-
let loading = $derived(!systemsProp && !!apiUrl && !!apiKey);
|
|
25
|
-
let fetchFailed = $state(false);
|
|
26
|
-
|
|
27
|
-
let usesApi = $derived(!systemsProp && !!apiUrl && !!apiKey);
|
|
28
|
-
|
|
29
|
-
let cachedClient: StatusClient | null = null;
|
|
30
|
-
let cachedApiUrl = '';
|
|
31
|
-
let cachedApiKey = '';
|
|
32
|
-
|
|
33
|
-
function getClient(): StatusClient {
|
|
34
|
-
if (!cachedClient || cachedApiUrl !== apiUrl || cachedApiKey !== apiKey) {
|
|
35
|
-
cachedClient = createStatusClient({ apiUrl: apiUrl!, apiKey: apiKey! });
|
|
36
|
-
cachedApiUrl = apiUrl!;
|
|
37
|
-
cachedApiKey = apiKey!;
|
|
38
|
-
}
|
|
39
|
-
return cachedClient;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function fetchData() {
|
|
43
|
-
if (!usesApi) return;
|
|
44
|
-
try {
|
|
45
|
-
const client = getClient();
|
|
46
|
-
const data = await client.getStatus();
|
|
47
|
-
systems = data.systems;
|
|
48
|
-
fetchFailed = false;
|
|
49
|
-
} catch {
|
|
50
|
-
fetchFailed = true;
|
|
51
|
-
} finally {
|
|
52
|
-
loading = false;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
onMount(() => {
|
|
57
|
-
fetchData();
|
|
58
|
-
|
|
59
|
-
let timer: ReturnType<typeof setInterval> | null = null;
|
|
60
|
-
if (usesApi && refreshInterval > 0) {
|
|
61
|
-
timer = setInterval(fetchData, refreshInterval * 1000);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return () => {
|
|
65
|
-
if (timer) clearInterval(timer);
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
let overallStatus = $derived(deriveOverallStatus(systems));
|
|
70
|
-
|
|
71
|
-
const messages: Record<SystemStatus, string> = {
|
|
72
|
-
operational: 'All systems operational',
|
|
73
|
-
degraded: 'Some systems degraded',
|
|
74
|
-
outage: 'System outage detected',
|
|
75
|
-
maintenance: 'Scheduled maintenance',
|
|
76
|
-
};
|
|
77
|
-
</script>
|
|
78
|
-
|
|
79
|
-
{#if loading}
|
|
80
|
-
<div class="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1.5 text-xs text-foreground-tertiary">
|
|
81
|
-
<div class="h-2 w-2 animate-pulse rounded-full bg-foreground-tertiary"></div>
|
|
82
|
-
<span>Loading...</span>
|
|
83
|
-
</div>
|
|
84
|
-
{:else if fetchFailed}
|
|
85
|
-
<button
|
|
86
|
-
onclick={() => { loading = true; fetchData(); }}
|
|
87
|
-
class="inline-flex items-center gap-2 rounded-full border border-red-500/30 px-3 py-1.5 text-xs text-red-400 transition-colors hover:bg-red-500/10"
|
|
88
|
-
>
|
|
89
|
-
<i class="fa-solid fa-circle-exclamation"></i>
|
|
90
|
-
<span>Status unavailable</span>
|
|
91
|
-
</button>
|
|
92
|
-
{:else if statusUrl}
|
|
93
|
-
<a
|
|
94
|
-
href={statusUrl}
|
|
95
|
-
class="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1.5 text-xs text-foreground-secondary transition-colors hover:bg-surface-hover"
|
|
96
|
-
>
|
|
97
|
-
<StatusIndicator status={overallStatus} size="sm" />
|
|
98
|
-
<span>{messages[overallStatus]}</span>
|
|
99
|
-
</a>
|
|
100
|
-
{:else}
|
|
101
|
-
<div class="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1.5 text-xs text-foreground-secondary">
|
|
102
|
-
<StatusIndicator status={overallStatus} size="sm" />
|
|
103
|
-
<span>{messages[overallStatus]}</span>
|
|
104
|
-
</div>
|
|
105
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { System, 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
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Systems data (pass-through mode) */
|
|
10
|
+
systems?: System[];
|
|
11
|
+
/** API URL for self-fetching mode */
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
/** API key for self-fetching mode */
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
/** Link to full status page */
|
|
16
|
+
statusUrl?: string;
|
|
17
|
+
/** Auto-refresh interval in seconds. 0 = disabled. Default: 60 */
|
|
18
|
+
refreshInterval?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { systems: systemsProp, apiUrl, apiKey, statusUrl, refreshInterval = 60 }: Props = $props();
|
|
22
|
+
|
|
23
|
+
let systems = $derived(systemsProp ?? []);
|
|
24
|
+
let loading = $derived(!systemsProp && !!apiUrl && !!apiKey);
|
|
25
|
+
let fetchFailed = $state(false);
|
|
26
|
+
|
|
27
|
+
let usesApi = $derived(!systemsProp && !!apiUrl && !!apiKey);
|
|
28
|
+
|
|
29
|
+
let cachedClient: StatusClient | null = null;
|
|
30
|
+
let cachedApiUrl = '';
|
|
31
|
+
let cachedApiKey = '';
|
|
32
|
+
|
|
33
|
+
function getClient(): StatusClient {
|
|
34
|
+
if (!cachedClient || cachedApiUrl !== apiUrl || cachedApiKey !== apiKey) {
|
|
35
|
+
cachedClient = createStatusClient({ apiUrl: apiUrl!, apiKey: apiKey! });
|
|
36
|
+
cachedApiUrl = apiUrl!;
|
|
37
|
+
cachedApiKey = apiKey!;
|
|
38
|
+
}
|
|
39
|
+
return cachedClient;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchData() {
|
|
43
|
+
if (!usesApi) return;
|
|
44
|
+
try {
|
|
45
|
+
const client = getClient();
|
|
46
|
+
const data = await client.getStatus();
|
|
47
|
+
systems = data.systems;
|
|
48
|
+
fetchFailed = false;
|
|
49
|
+
} catch {
|
|
50
|
+
fetchFailed = true;
|
|
51
|
+
} finally {
|
|
52
|
+
loading = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onMount(() => {
|
|
57
|
+
fetchData();
|
|
58
|
+
|
|
59
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
60
|
+
if (usesApi && refreshInterval > 0) {
|
|
61
|
+
timer = setInterval(fetchData, refreshInterval * 1000);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
if (timer) clearInterval(timer);
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let overallStatus = $derived(deriveOverallStatus(systems));
|
|
70
|
+
|
|
71
|
+
const messages: Record<SystemStatus, string> = {
|
|
72
|
+
operational: 'All systems operational',
|
|
73
|
+
degraded: 'Some systems degraded',
|
|
74
|
+
outage: 'System outage detected',
|
|
75
|
+
maintenance: 'Scheduled maintenance',
|
|
76
|
+
};
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
{#if loading}
|
|
80
|
+
<div class="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1.5 text-xs text-foreground-tertiary">
|
|
81
|
+
<div class="h-2 w-2 animate-pulse rounded-full bg-foreground-tertiary"></div>
|
|
82
|
+
<span>Loading...</span>
|
|
83
|
+
</div>
|
|
84
|
+
{:else if fetchFailed}
|
|
85
|
+
<button
|
|
86
|
+
onclick={() => { loading = true; fetchData(); }}
|
|
87
|
+
class="inline-flex items-center gap-2 rounded-full border border-red-500/30 px-3 py-1.5 text-xs text-red-400 transition-colors hover:bg-red-500/10"
|
|
88
|
+
>
|
|
89
|
+
<i class="fa-solid fa-circle-exclamation"></i>
|
|
90
|
+
<span>Status unavailable</span>
|
|
91
|
+
</button>
|
|
92
|
+
{:else if statusUrl}
|
|
93
|
+
<a
|
|
94
|
+
href={statusUrl}
|
|
95
|
+
class="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1.5 text-xs text-foreground-secondary transition-colors hover:bg-surface-hover"
|
|
96
|
+
>
|
|
97
|
+
<StatusIndicator status={overallStatus} size="sm" />
|
|
98
|
+
<span>{messages[overallStatus]}</span>
|
|
99
|
+
</a>
|
|
100
|
+
{:else}
|
|
101
|
+
<div class="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1.5 text-xs text-foreground-secondary">
|
|
102
|
+
<StatusIndicator status={overallStatus} size="sm" />
|
|
103
|
+
<span>{messages[overallStatus]}</span>
|
|
104
|
+
</div>
|
|
105
|
+
{/if}
|