@alliance-droid/status-feedback-system 0.3.3 → 1.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 ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ First stable release. Client-only package with full component suite.
6
+
7
+ ### Components
8
+ - `StatusPage` — Full status page with systems, incidents, uptime bars (self-fetching or pass-through)
9
+ - `StatusBanner` — Compact inline status indicator
10
+ - `FeedbackButton` — Floating action button with embedded feedback form
11
+ - `FeedbackForm` — Standalone feedback submission form
12
+ - `FeedbackBoard` — Public feedback board with voting, categories, filtering
13
+ - `DevFeedback` — Developer feedback overlay (Ctrl+right-click) with screenshot capture
14
+ - Shared: `StatusIndicator`, `UptimeBar`, `IncidentTimeline`, `SystemCard`
15
+
16
+ ### API Client
17
+ - `createStatusClient()` with typed methods for all endpoints
18
+ - Custom `fetch` support for SSR
19
+
20
+ ### Quality
21
+ - 150 tests across 13 test files
22
+ - 0 svelte-check errors/warnings
23
+ - Full TypeScript types for all exports
24
+ - Comprehensive README with architecture docs and props tables
25
+
26
+ ## 0.4.0
27
+
28
+ ### Breaking Changes
29
+ - Package is now **client-only** — server/admin components moved to `status-feedback-admin` app
30
+ - Removed `./server` and `./admin` subpath exports
31
+ - All data is fetched from the admin backend via `createStatusClient` API
32
+
33
+ ### Added
34
+ - `DevFeedback` — Ctrl+right-click developer feedback with screenshot capture, console error collection, and environment auto-detection
35
+ - `FeedbackBoard` — Public feedback board with voting, categories, filtering, and inline responses
36
+ - `FeedbackButton` — Floating action button with embedded feedback form
37
+ - `StatusBanner` — Compact inline status indicator (self-fetching or pass-through)
38
+ - `StatusPage` — Full status page with systems, incidents, uptime bars (self-fetching or pass-through)
39
+ - `FeedbackForm` — Standalone feedback submission form
40
+ - Shared components: `StatusIndicator`, `UptimeBar`, `IncidentTimeline`, `SystemCard`
41
+ - Full API client (`createStatusClient`) with typed methods for all endpoints
42
+
43
+ ### Fixed
44
+ - Proper TypeScript types for all event handlers
45
+ - Accessibility: aria-labels on all icon-only buttons
46
+
47
+ ## 0.1.1
48
+
49
+ - Initial client package extraction from monolith
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alliance Technical Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,95 +1,271 @@
1
1
  # @alliance-droid/status-feedback-system
2
2
 
3
- Drop-in system status pages and user feedback forms for SvelteKit apps.
3
+ Drop-in system status pages and user feedback components for SvelteKit apps.
4
4
 
5
- ## Features
5
+ This is the **client package** — UI components that talk to a [status-feedback-admin](https://github.com/alliance-droid/status-feedback-admin) backend via API. The admin app handles data storage, API key management, and the admin dashboard.
6
6
 
7
- - **Status Page** — Public-facing system health display with uptime bars
8
- - **Feedback Form** — Users submit bugs, feature requests, general feedback
9
- - **Admin Dashboard** — Manage projects, systems, incidents, and feedback
10
- - **API Key Auth** — Multi-project support with scoped API keys
11
- - **MSSQL** — Auto-migration, all tables prefixed `sf_`
7
+ ## Architecture
12
8
 
13
- ## Quick Start
9
+ ```
10
+ ┌─────────────────────────┐ ┌──────────────────────────┐
11
+ │ Your SvelteKit App │ │ status-feedback-admin │
12
+ │ │ │ │
13
+ │ ┌───────────────────┐ │ API │ ┌────────────────────┐ │
14
+ │ │ StatusPage │──┼───────┼──│ /api/status │ │
15
+ │ │ FeedbackButton │ │ │ │ /api/feedback │ │
16
+ │ │ DevFeedback │ │ │ │ /api/incidents │ │
17
+ │ │ FeedbackBoard │ │ │ │ /api/board │ │
18
+ │ └───────────────────┘ │ │ └────────────────────┘ │
19
+ │ │ │ │ │
20
+ │ npm package │ │ MSSQL + Azure Blob │
21
+ └─────────────────────────┘ └──────────────────────────┘
22
+ ```
14
23
 
15
- ### Server Setup
24
+ ## Setup
16
25
 
17
- ```typescript
18
- // src/lib/server/status.ts
19
- import { createStatusServer } from '@alliance-droid/status-feedback-system/server';
26
+ ### 1. Deploy the admin backend
20
27
 
21
- export const statusServer = createStatusServer({
22
- database: { connectionString: env.MSSQL_CONNECTION_STRING }
23
- });
28
+ The admin app ([status-feedback-admin](https://github.com/alliance-droid/status-feedback-admin)) provides the API, database, and admin dashboard. Deploy it first.
29
+
30
+ Required env vars on the admin app:
31
+ ```bash
32
+ MSSQL_CONNECTION_STRING=Server=...;Initial Catalog=StatusFeedback;...
33
+ SESSION_SECRET=your-secret
24
34
 
25
- await statusServer.init();
35
+ # Optional — stores screenshots in Azure Blob Storage instead of base64 in DB
36
+ AZURE_BLOB_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
37
+ AZURE_BLOB_CONTAINER=screenshots
26
38
  ```
27
39
 
28
- ### API Routes
40
+ ### 2. Create a project and get an API key
29
41
 
30
- ```typescript
31
- // src/routes/api/status/[...path]/+server.ts
32
- import { statusServer } from '$lib/server/status';
33
-
34
- const handlers = statusServer.createHandlers();
35
- export const GET = handlers.GET;
36
- export const POST = handlers.POST;
37
- export const PATCH = handlers.PATCH;
38
- export const DELETE = handlers.DELETE;
42
+ Log into the admin dashboard, create a project, and copy its API key (prefixed `sf_`).
43
+
44
+ ### 3. Install the client package
45
+
46
+ ```bash
47
+ npm install @alliance-droid/status-feedback-system
39
48
  ```
40
49
 
41
- ### Status Page
50
+ **Peer dependencies:** `svelte ^5.0.0`, `@alliance-droid/svelte-component-library >=1.0.0`
51
+
52
+ ## Components
53
+
54
+ ### StatusPage
55
+
56
+ Full status page with systems, incidents, and uptime history bars. Supports **self-fetching** (provide `apiUrl` + `apiKey`) or **pass-through** (provide data directly).
42
57
 
43
58
  ```svelte
44
59
  <script>
45
60
  import { StatusPage } from '@alliance-droid/status-feedback-system';
46
- let { data } = $props();
47
61
  </script>
48
62
 
63
+ <!-- Self-fetching mode -->
64
+ <StatusPage
65
+ projectName="My App"
66
+ apiUrl="https://status-admin.example.com"
67
+ apiKey="sf_abc123..."
68
+ />
69
+
70
+ <!-- Pass-through mode -->
49
71
  <StatusPage
50
72
  projectName="My App"
51
73
  systems={data.systems}
52
74
  incidents={data.incidents}
53
75
  statusHistory={data.statusHistory}
76
+ showUptime={true}
54
77
  />
55
78
  ```
56
79
 
57
- ### Feedback Button
80
+ | Prop | Type | Default | Description |
81
+ |------|------|---------|-------------|
82
+ | `projectName` | `string` | required | Display name in the header |
83
+ | `apiUrl` | `string` | — | Admin backend URL (self-fetching mode) |
84
+ | `apiKey` | `string` | — | Project API key (self-fetching mode) |
85
+ | `systems` | `System[]` | — | Pass-through: system data |
86
+ | `incidents` | `Incident[]` | — | Pass-through: incident data |
87
+ | `statusHistory` | `Record<string, StatusHistoryEntry[]>` | — | Pass-through: uptime data |
88
+ | `showUptime` | `boolean` | `true` | Show 90-day uptime bars |
89
+
90
+ ### StatusBanner
91
+
92
+ Compact inline status indicator — good for footers or nav bars.
58
93
 
59
94
  ```svelte
60
- <script>
61
- import { FeedbackButton } from '@alliance-droid/status-feedback-system';
62
- </script>
95
+ <StatusBanner
96
+ apiUrl="https://status-admin.example.com"
97
+ apiKey="sf_abc123..."
98
+ statusUrl="/status"
99
+ />
100
+ ```
101
+
102
+ | Prop | Type | Default | Description |
103
+ |------|------|---------|-------------|
104
+ | `systems` | `System[]` | — | Pass-through mode |
105
+ | `apiUrl` | `string` | — | Self-fetching mode |
106
+ | `apiKey` | `string` | — | Self-fetching mode |
107
+ | `statusUrl` | `string` | — | Links to full status page (renders as `<a>`) |
108
+
109
+ ### FeedbackButton
110
+
111
+ Floating action button with an embedded feedback form. Fixed position, bottom corner.
63
112
 
113
+ ```svelte
64
114
  <FeedbackButton
65
115
  apiKey="sf_abc123..."
66
- endpoint="/api/status"
116
+ apiUrl="https://status-admin.example.com"
67
117
  position="bottom-right"
118
+ categories={['Bug', 'Feature Request', 'General']}
119
+ userEmail={currentUser?.email}
68
120
  />
69
121
  ```
70
122
 
71
- ### Admin Dashboard
123
+ | Prop | Type | Default | Description |
124
+ |------|------|---------|-------------|
125
+ | `apiUrl` | `string` | required | Admin backend URL |
126
+ | `apiKey` | `string` | required | Project API key |
127
+ | `categories` | `string[]` | `['Bug', 'Feature Request', 'General']` | Category options |
128
+ | `userEmail` | `string` | — | Pre-fill email field |
129
+ | `position` | `'bottom-right' \| 'bottom-left'` | `'bottom-right'` | Button position |
130
+
131
+ ### FeedbackForm
132
+
133
+ Standalone feedback form — embed it wherever you need it.
134
+
135
+ ```svelte
136
+ <FeedbackForm
137
+ apiKey="sf_abc123..."
138
+ apiUrl="https://status-admin.example.com"
139
+ categories={['Bug', 'Feature Request']}
140
+ userEmail={currentUser?.email}
141
+ onSuccess={() => toast('Thanks!')}
142
+ onError={(msg) => toast(msg, 'error')}
143
+ />
144
+ ```
145
+
146
+ | Prop | Type | Default | Description |
147
+ |------|------|---------|-------------|
148
+ | `apiUrl` | `string` | required | Admin backend URL |
149
+ | `apiKey` | `string` | required | Project API key |
150
+ | `categories` | `string[]` | `['Bug', 'Feature Request', 'General']` | Category options |
151
+ | `userEmail` | `string` | — | Pre-fill email field |
152
+ | `onSuccess` | `() => void` | — | Called after successful submission |
153
+ | `onError` | `(error: string) => void` | — | Called on submission error |
154
+
155
+ ### FeedbackBoard
156
+
157
+ Public feedback board with voting, category filtering, and admin responses.
158
+
159
+ ```svelte
160
+ <FeedbackBoard
161
+ apiUrl="https://status-admin.example.com"
162
+ apiKey="sf_abc123..."
163
+ title="Feature Requests"
164
+ showSubmit={true}
165
+ submitCategories={['Bug', 'Feature Request', 'Improvement']}
166
+ categoryConfig={{
167
+ Bug: { icon: 'fa-solid fa-bug', label: 'Bugs', description: 'Something broken' },
168
+ 'Feature Request': { icon: 'fa-solid fa-lightbulb', label: 'Ideas' },
169
+ }}
170
+ />
171
+ ```
172
+
173
+ | Prop | Type | Default | Description |
174
+ |------|------|---------|-------------|
175
+ | `apiUrl` | `string` | required | Admin backend URL |
176
+ | `apiKey` | `string` | required | Project API key |
177
+ | `title` | `string` | `'Feedback Board'` | Header title |
178
+ | `showSubmit` | `boolean` | `true` | Show inline submit form |
179
+ | `submitCategories` | `string[]` | `[]` | Category options for submit form |
180
+ | `categoryConfig` | `Record<string, { label?, icon?, description? }>` | `{}` | Customize sidebar categories |
181
+
182
+ ### DevFeedback
183
+
184
+ Developer feedback overlay for staging/preview environments. Activated via **Ctrl+right-click** anywhere on the page.
185
+
186
+ Auto-captures: screenshots (via html2canvas), console errors, viewport size, current route, clicked element selector, and deployment context (Vercel env vars).
72
187
 
73
188
  ```svelte
74
189
  <script>
75
- import { AdminDashboard } from '@alliance-droid/status-feedback-system/admin';
76
- let { data } = $props();
190
+ import { DevFeedback } from '@alliance-droid/status-feedback-system';
77
191
  </script>
78
192
 
79
- <AdminDashboard
80
- projects={data.projects}
81
- apiEndpoint="/api/status"
193
+ <!-- In your root +layout.svelte -->
194
+ <DevFeedback
195
+ apiUrl="https://status-admin.example.com"
196
+ apiKey="sf_abc123..."
197
+ boardUrl="/feedback"
82
198
  />
83
199
  ```
84
200
 
85
- ## Exports
201
+ | Prop | Type | Default | Description |
202
+ |------|------|---------|-------------|
203
+ | `apiUrl` | `string` | required | Admin backend URL |
204
+ | `apiKey` | `string` | required | Project API key |
205
+ | `enabled` | `boolean` | auto-detect | Override environment detection |
206
+ | `boardUrl` | `string` | — | URL for "View Open Issues" link. Hidden if omitted. |
207
+ | `maxConsoleErrors` | `number` | `20` | Max console errors to capture |
208
+ | `showOnboarding` | `boolean` | `true` | Show "Ctrl+right-click" toast on first visit |
209
+
210
+ **Environment auto-detection:** Enabled on `localhost`, `127.0.0.1`, `*.local`, and Vercel preview deployments. Disabled in Vercel production. Override with the `enabled` prop or `PUBLIC_FEEDBACK_ENABLED` env var.
211
+
212
+ **Screenshot storage:** If the admin backend has `AZURE_BLOB_CONNECTION_STRING` configured, screenshots are uploaded to Azure Blob Storage. Otherwise they're stored as base64 in the database.
213
+
214
+ ## API Client
215
+
216
+ All components use a shared API client. You can also use it directly:
86
217
 
87
- | Path | Description |
88
- |------|-------------|
89
- | `.` | Client components (StatusPage, FeedbackForm, etc.) |
90
- | `./server` | Server module (createStatusServer, types) |
91
- | `./admin` | Admin components (AdminDashboard, etc.) |
218
+ ```typescript
219
+ import { createStatusClient } from '@alliance-droid/status-feedback-system';
220
+
221
+ const client = createStatusClient({
222
+ apiUrl: 'https://status-admin.example.com',
223
+ apiKey: 'sf_abc123...',
224
+ fetch: event.fetch, // Optional: pass SvelteKit's fetch for SSR
225
+ });
226
+
227
+ const { systems, overallStatus } = await client.getStatus();
228
+ const { incidents } = await client.getIncidents({ limit: 5, includeResolved: true });
229
+ const { history } = await client.getUptime({ days: 90 });
230
+ const feedback = await client.submitFeedback({ message: 'Great product!' });
231
+ const { items, total, categories } = await client.getBoard({ sort: 'votes', status: 'open' });
232
+ ```
233
+
234
+ ## Shared Components
235
+
236
+ Lower-level building blocks used by StatusPage — exported for custom layouts:
237
+
238
+ ```typescript
239
+ import {
240
+ StatusIndicator, // Status dot + label (operational/degraded/outage/maintenance)
241
+ UptimeBar, // 90-day uptime bar chart
242
+ IncidentTimeline, // Chronological incident updates
243
+ SystemCard, // Single system status card with uptime bar
244
+ } from '@alliance-droid/status-feedback-system';
245
+ ```
246
+
247
+ ## All Exports
248
+
249
+ ```typescript
250
+ // Components
251
+ import {
252
+ StatusPage, StatusBanner, FeedbackForm, FeedbackButton,
253
+ FeedbackBoard, DevFeedback,
254
+ StatusIndicator, UptimeBar, IncidentTimeline, SystemCard,
255
+ } from '@alliance-droid/status-feedback-system';
256
+
257
+ // API Client
258
+ import { createStatusClient } from '@alliance-droid/status-feedback-system';
259
+ import type { StatusClient, StatusClientConfig } from '@alliance-droid/status-feedback-system';
260
+
261
+ // Types
262
+ import type {
263
+ System, Incident, IncidentUpdate, Feedback, FeedbackResponse,
264
+ SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus,
265
+ StatusData, IncidentData, UptimeData, BoardData, BoardItem,
266
+ } from '@alliance-droid/status-feedback-system';
267
+ ```
92
268
 
93
269
  ## License
94
270
 
95
- UNLICENSED — Private package.
271
+ MIT © 2026 Alliance Technical Group
@@ -20,6 +20,8 @@
20
20
  maxConsoleErrors?: number;
21
21
  /** Show onboarding toast on first visit */
22
22
  showOnboarding?: boolean;
23
+ /** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
24
+ boardUrl?: string;
23
25
  }
24
26
 
25
27
  let {
@@ -28,6 +30,7 @@
28
30
  enabled = undefined,
29
31
  maxConsoleErrors = 20,
30
32
  showOnboarding = true,
33
+ boardUrl,
31
34
  }: Props = $props();
32
35
 
33
36
  let client = $derived(createStatusClient({ apiUrl, apiKey }));
@@ -43,7 +46,7 @@
43
46
  } catch { /* ignore */ }
44
47
  try {
45
48
  // Window-injected env (common pattern)
46
- const win = window as Record<string, unknown>;
49
+ const win = window as unknown as Record<string, unknown>;
47
50
  const env = win.__ENV as Record<string, string> | undefined;
48
51
  if (env?.[key]) return env[key];
49
52
  } catch { /* ignore */ }
@@ -159,6 +162,7 @@
159
162
  const parts: string[] = [];
160
163
  let current: HTMLElement | null = el;
161
164
  while (current && current !== document.body) {
165
+ if (!current.tagName) { current = current.parentElement; continue; }
162
166
  let selector = current.tagName.toLowerCase();
163
167
  if (current.id) {
164
168
  selector += `#${current.id}`;
@@ -195,8 +199,8 @@
195
199
  windowHeight: window.innerHeight,
196
200
  });
197
201
  screenshotData = canvas.toDataURL('image/png');
198
- } catch (e) {
199
- console.warn('Screenshot capture failed:', e);
202
+ } catch {
203
+ // Screenshot capture can fail in cross-origin iframes or restricted contexts
200
204
  screenshotData = null;
201
205
  } finally {
202
206
  capturingScreenshot = false;
@@ -285,8 +289,9 @@
285
289
  }
286
290
 
287
291
  function openBoard() {
292
+ if (!boardUrl) return;
288
293
  menuOpen = false;
289
- window.open('/feedback', '_blank');
294
+ window.open(boardUrl, '_blank');
290
295
  }
291
296
 
292
297
  // Ensure menu stays in viewport
@@ -297,8 +302,7 @@
297
302
  {#if isEnabled}
298
303
  <!-- Context menu -->
299
304
  {#if menuOpen}
300
- <!-- svelte-ignore a11y_no_static_element_interactions -->
301
- <!-- svelte-ignore a11y_no_static_element_interactions -->
305
+ <!-- svelte-ignore a11y_no_static_element_interactions — context menu needs mousedown to prevent close-on-click -->
302
306
  <div
303
307
  class="fixed z-[99999]"
304
308
  style="left: {adjustedX}px; top: {adjustedY}px;"
@@ -313,22 +317,23 @@
313
317
  <i class="fa-solid fa-lightbulb text-amber-400"></i>
314
318
  Request Feature
315
319
  </button>
316
- <div class="sf-ctx-divider">
317
- <button class="sf-ctx-item sf-ctx-item-secondary" onclick={openBoard}>
318
- <i class="fa-solid fa-clipboard-list"></i>
319
- View Open Issues
320
- </button>
321
- </div>
320
+ {#if boardUrl}
321
+ <div class="sf-ctx-divider">
322
+ <button class="sf-ctx-item sf-ctx-item-secondary" onclick={openBoard}>
323
+ <i class="fa-solid fa-clipboard-list"></i>
324
+ View Open Issues
325
+ </button>
326
+ </div>
327
+ {/if}
322
328
  </div>
323
329
  </div>
324
330
  {/if}
325
331
 
326
332
  <!-- Form modal -->
327
333
  {#if formOpen}
328
- <!-- svelte-ignore a11y_no_static_element_interactions -->
329
- <!-- svelte-ignore a11y_no_static_element_interactions -->
334
+ <!-- svelte-ignore a11y_no_static_element_interactions — modal backdrop needs mousedown for click-outside-to-close -->
330
335
  <div class="fixed inset-0 z-[99998] flex items-center justify-center bg-black/50 backdrop-blur-sm" onmousedown={(e) => { if (e.target === e.currentTarget) formOpen = false; }}>
331
- <!-- svelte-ignore a11y_no_static_element_interactions -->
336
+ <!-- svelte-ignore a11y_no_static_element_interactions — prevents modal close when clicking inside form -->
332
337
  <div class="w-full max-w-lg mx-4" onmousedown={(e) => e.stopPropagation()}>
333
338
  <Card variant="elevated" padding="none">
334
339
  <!-- Header -->
@@ -346,6 +351,7 @@
346
351
  <button
347
352
  class="text-foreground-tertiary hover:text-foreground transition-colors bg-transparent border-none cursor-pointer text-lg"
348
353
  onclick={() => (formOpen = false)}
354
+ aria-label="Close feedback form"
349
355
  >
350
356
  <i class="fa-solid fa-xmark"></i>
351
357
  </button>
@@ -369,7 +375,7 @@
369
375
  />
370
376
 
371
377
  <Textarea
372
- bind:value={formMessage}
378
+ value={formMessage} oninput={(e) => (formMessage = (e.currentTarget as HTMLTextAreaElement).value)}
373
379
  placeholder={formType === 'bug'
374
380
  ? 'What happened? What did you expect?'
375
381
  : 'Describe the feature you\'d like...'}
@@ -426,6 +432,7 @@
426
432
  <button
427
433
  class="absolute top-1 right-1 bg-black/60 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs border-none cursor-pointer hover:bg-black/80"
428
434
  onclick={() => (screenshotData = null)}
435
+ aria-label="Remove screenshot"
429
436
  >
430
437
  <i class="fa-solid fa-xmark"></i>
431
438
  </button>
@@ -442,7 +449,7 @@
442
449
  {/if}
443
450
 
444
451
  <div class="flex justify-end gap-2 pt-1">
445
- <Button variant="neutral" style="outlined" onclick={() => (formOpen = false)}>
452
+ <Button variant="neutral" style="outline" onclick={() => (formOpen = false)}>
446
453
  Cancel
447
454
  </Button>
448
455
  <Button
@@ -470,7 +477,7 @@
470
477
  <p style="font-size: 0.875rem; font-weight: 600; margin: 0;">Ctrl + Right-click anywhere</p>
471
478
  <p style="font-size: 0.75rem; color: #94a3b8; margin: 0;">to report bugs or request features</p>
472
479
  </div>
473
- <button class="sf-toast-close" onclick={() => (showToast = false)}>
480
+ <button class="sf-toast-close" onclick={() => (showToast = false)} aria-label="Dismiss">
474
481
  <i class="fa-solid fa-xmark"></i>
475
482
  </button>
476
483
  </div>
@@ -7,6 +7,8 @@ interface Props {
7
7
  maxConsoleErrors?: number;
8
8
  /** Show onboarding toast on first visit */
9
9
  showOnboarding?: boolean;
10
+ /** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
11
+ boardUrl?: string;
10
12
  }
11
13
  declare const DevFeedback: import("svelte").Component<Props, {}, "">;
12
14
  type DevFeedback = ReturnType<typeof DevFeedback>;
@@ -56,6 +56,7 @@
56
56
  let submitEmail = $state('');
57
57
  let submitting = $state(false);
58
58
  let submitSuccess = $state(false);
59
+ let submitError = $state('');
59
60
 
60
61
  let votedItems = $state<Record<string, 'up' | 'down'>>({});
61
62
 
@@ -98,7 +99,7 @@
98
99
  votedItems = { ...votedItems, [itemId]: direction };
99
100
  }
100
101
  } catch {
101
- // silently fail
102
+ // Vote failures are silent — optimistic UI, vote just doesn't persist
102
103
  }
103
104
  }
104
105
 
@@ -114,6 +115,7 @@
114
115
  const data = await client.getResponses(itemId);
115
116
  responses = { ...responses, [itemId]: data.responses };
116
117
  } catch {
118
+ // Show empty responses on failure — user can re-expand to retry
117
119
  responses = { ...responses, [itemId]: [] };
118
120
  } finally {
119
121
  loadingResponses = { ...loadingResponses, [itemId]: false };
@@ -124,13 +126,14 @@
124
126
  async function handleSubmit() {
125
127
  if (!submitMessage.trim()) return;
126
128
  submitting = true;
129
+ submitError = '';
127
130
  try {
128
131
  await client.submitFeedback({
129
132
  title: submitTitle.trim() || undefined,
130
133
  message: submitMessage.trim(),
131
134
  category: submitCategory || undefined,
132
135
  email: submitEmail.trim() || undefined,
133
- pageUrl: window.location.href,
136
+ pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
134
137
  });
135
138
  submitSuccess = true;
136
139
  submitTitle = '';
@@ -142,8 +145,8 @@
142
145
  showForm = false;
143
146
  }, 2000);
144
147
  await load();
145
- } catch {
146
- // ignore
148
+ } catch (err) {
149
+ submitError = err instanceof Error ? err.message : 'Failed to submit feedback';
147
150
  } finally {
148
151
  submitting = false;
149
152
  }
@@ -271,10 +274,16 @@
271
274
  placeholder="Title (optional)"
272
275
  />
273
276
  <Textarea
274
- bind:value={submitMessage}
277
+ value={submitMessage} oninput={(e) => (submitMessage = (e.currentTarget as HTMLTextAreaElement).value)}
275
278
  placeholder="Describe your feedback..."
276
279
  rows={3}
277
280
  />
281
+ {#if submitError}
282
+ <p class="text-sm text-red-400">
283
+ <i class="fa-solid fa-exclamation-circle mr-1"></i>
284
+ {submitError}
285
+ </p>
286
+ {/if}
278
287
  <div class="flex gap-2 items-center flex-wrap">
279
288
  {#if submitCategoryOptions.length > 0}
280
289
  <div class="min-w-[140px]">
@@ -333,7 +342,7 @@
333
342
  size="sm"
334
343
  variant={votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
335
344
  onclick={() => handleVote(item.id, 'up')}
336
- aria-label="Upvote"
345
+ label="Upvote"
337
346
  >
338
347
  <i class="fa-solid fa-chevron-up text-xs"></i>
339
348
  </IconButton>
@@ -342,7 +351,7 @@
342
351
  size="sm"
343
352
  variant={votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
344
353
  onclick={() => handleVote(item.id, 'down')}
345
- aria-label="Downvote"
354
+ label="Downvote"
346
355
  >
347
356
  <i class="fa-solid fa-chevron-down text-xs"></i>
348
357
  </IconButton>
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { createStatusClient } from '../api.js';
4
+
2
5
  interface Props {
3
6
  apiKey: string;
4
7
  apiUrl: string;
@@ -17,9 +20,11 @@
17
20
  onError,
18
21
  }: Props = $props();
19
22
 
20
- let category = $state(categories[0] || '');
23
+ let client = $derived(createStatusClient({ apiUrl, apiKey }));
24
+
25
+ let category = $state(untrack(() => categories[0] || ''));
21
26
  let message = $state('');
22
- let email = $state(userEmail ?? '');
27
+ let email = $state(untrack(() => userEmail ?? ''));
23
28
  let submitting = $state(false);
24
29
  let submitted = $state(false);
25
30
  let errorMsg = $state('');
@@ -32,25 +37,13 @@
32
37
  errorMsg = '';
33
38
 
34
39
  try {
35
- const res = await fetch(`${apiUrl.replace(/\/$/, '')}/api/feedback`, {
36
- method: 'POST',
37
- headers: {
38
- 'Content-Type': 'application/json',
39
- 'x-api-key': apiKey,
40
- },
41
- body: JSON.stringify({
42
- category,
43
- message: message.trim(),
44
- email: email.trim() || undefined,
45
- pageUrl: window.location.href,
46
- }),
40
+ await client.submitFeedback({
41
+ category,
42
+ message: message.trim(),
43
+ email: email.trim() || undefined,
44
+ pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
47
45
  });
48
46
 
49
- if (!res.ok) {
50
- const data = await res.json();
51
- throw new Error(data.error || 'Failed to submit feedback');
52
- }
53
-
54
47
  submitted = true;
55
48
  onSuccess?.();
56
49
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, untrack } from 'svelte';
3
3
  import type { System, SystemStatus } from '../types.js';
4
4
  import { createStatusClient } from '../api.js';
5
5
  import StatusIndicator from './shared/StatusIndicator.svelte';
@@ -17,8 +17,8 @@
17
17
 
18
18
  let { systems: systemsProp, apiUrl, apiKey, statusUrl }: Props = $props();
19
19
 
20
- let systems: System[] = $state(systemsProp ?? []);
21
- let loading = $state(!systemsProp && !!apiUrl && !!apiKey);
20
+ let systems: System[] = $state(untrack(() => systemsProp ?? []));
21
+ let loading = $state(untrack(() => !systemsProp && !!apiUrl && !!apiKey));
22
22
 
23
23
  onMount(async () => {
24
24
  if (systemsProp || !apiUrl || !apiKey) return;
@@ -26,8 +26,9 @@
26
26
  const client = createStatusClient({ apiUrl, apiKey });
27
27
  const data = await client.getStatus();
28
28
  systems = data.systems;
29
- } catch (err) {
30
- console.error('StatusBanner fetch error:', err);
29
+ } catch {
30
+ // Error is silent — banner simply stays in loading state
31
+ // Consumer can use StatusPage for full error display
31
32
  } finally {
32
33
  loading = false;
33
34
  }
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, untrack } from 'svelte';
3
3
  import type { System, Incident, StatusHistoryEntry, SystemStatus } from '../types.js';
4
4
  import { createStatusClient } from '../api.js';
5
5
  import StatusIndicator from './shared/StatusIndicator.svelte';
@@ -33,10 +33,10 @@
33
33
  }: Props = $props();
34
34
 
35
35
  // ── State ──
36
- let systems: System[] = $state(systemsProp ?? []);
37
- let incidents: Incident[] = $state(incidentsProp ?? []);
38
- let statusHistory: Record<string, StatusHistoryEntry[]> = $state(statusHistoryProp ?? {});
39
- let loading = $state(!systemsProp);
36
+ let systems: System[] = $state(untrack(() => systemsProp ?? []));
37
+ let incidents: Incident[] = $state(untrack(() => incidentsProp ?? []));
38
+ let statusHistory: Record<string, StatusHistoryEntry[]> = $state(untrack(() => statusHistoryProp ?? {}));
39
+ let loading = $state(untrack(() => !systemsProp));
40
40
  let error = $state('');
41
41
 
42
42
  // ── Self-fetching mode ──
@@ -59,7 +59,6 @@
59
59
  statusHistory = uptimeData.history;
60
60
  } catch (err) {
61
61
  error = err instanceof Error ? err.message : 'Failed to load status data';
62
- console.error('StatusPage fetch error:', err);
63
62
  } finally {
64
63
  loading = false;
65
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/status-feedback-system",
3
- "version": "0.3.3",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
6
6
  "svelte": "./dist/index.js",
@@ -13,40 +13,38 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
- "!dist/**/*.test.*"
16
+ "!dist/**/*.test.*",
17
+ "LICENSE",
18
+ "CHANGELOG.md"
17
19
  ],
18
20
  "scripts": {
19
- "dev": "vite dev",
20
- "build": "vite build && npm run package",
21
- "package": "svelte-kit sync && svelte-package -o dist",
22
- "preview": "vite preview",
23
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
21
+ "build": "svelte-package -o dist && publint",
22
+ "check": "svelte-check --tsconfig ./tsconfig.json",
24
23
  "test": "vitest run",
25
24
  "test:watch": "vitest",
26
- "prepublishOnly": "npm run package"
25
+ "prepublishOnly": "npm run build"
27
26
  },
28
27
  "peerDependencies": {
29
- "@alliance-droid/svelte-component-library": ">=0.1.0",
28
+ "@alliance-droid/svelte-component-library": ">=1.0.0",
30
29
  "svelte": "^5.0.0"
31
30
  },
32
31
  "devDependencies": {
33
- "@alliance-droid/svelte-component-library": "^0.1.88",
34
- "@sveltejs/adapter-auto": "^7.0.0",
35
- "@sveltejs/kit": "^2.50.0",
32
+ "@alliance-droid/svelte-component-library": "^1.0.5",
36
33
  "@sveltejs/package": "^2.3.0",
37
34
  "@sveltejs/vite-plugin-svelte": "^6.0.0",
38
- "@tailwindcss/vite": "^4.1.0",
39
35
  "@testing-library/jest-dom": "^6.9.1",
40
36
  "@testing-library/svelte": "^5.3.1",
41
37
  "jsdom": "^29.0.0",
42
38
  "publint": "^0.3.0",
43
39
  "svelte": "^5.48.0",
44
40
  "svelte-check": "^4.3.0",
45
- "tailwindcss": "^4.1.0",
46
41
  "typescript": "^5.9.0",
47
42
  "vite": "^7.3.0",
48
43
  "vitest": "^4.0.0"
49
44
  },
45
+ "dependencies": {
46
+ "html2canvas": "^1.4.1"
47
+ },
50
48
  "keywords": [
51
49
  "svelte",
52
50
  "sveltekit",
@@ -55,12 +53,12 @@
55
53
  "uptime"
56
54
  ],
57
55
  "author": "Alliance Technical Group",
58
- "license": "UNLICENSED",
56
+ "license": "MIT",
59
57
  "publishConfig": {
60
58
  "access": "restricted"
61
59
  },
62
- "dependencies": {
63
- "@alliance-droid/svelte-component-library": ">=0.1.0",
64
- "html2canvas": "^1.4.1"
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/alliance-droid/status-feedback-system.git"
65
63
  }
66
64
  }