@alliance-droid/status-feedback-system 0.3.3 → 1.0.1
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 +49 -0
- package/LICENSE +21 -0
- package/README.md +222 -46
- package/dist/components/DevFeedback.svelte +25 -18
- package/dist/components/DevFeedback.svelte.d.ts +2 -0
- package/dist/components/FeedbackBoard.svelte +16 -7
- package/dist/components/FeedbackForm.svelte +12 -19
- package/dist/components/StatusBanner.svelte +6 -5
- package/dist/components/StatusPage.svelte +5 -6
- package/package.json +16 -18
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
|
|
3
|
+
Drop-in system status pages and user feedback components for SvelteKit apps.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
## Setup
|
|
16
25
|
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
35
|
+
# Optional — stores screenshots in Azure Blob Storage instead of base64 in DB
|
|
36
|
+
AZURE_FEEDBACK_BLOB_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
|
|
37
|
+
AZURE_FEEDBACK_BLOB_CONTAINER=screenshots
|
|
26
38
|
```
|
|
27
39
|
|
|
28
|
-
### API
|
|
40
|
+
### 2. Create a project and get an API key
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
76
|
-
let { data } = $props();
|
|
190
|
+
import { DevFeedback } from '@alliance-droid/status-feedback-system';
|
|
77
191
|
</script>
|
|
78
192
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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_FEEDBACK_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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
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(
|
|
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
|
-
|
|
317
|
-
<
|
|
318
|
-
<
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
30
|
-
|
|
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
|
+
"version": "1.0.1",
|
|
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
|
-
"
|
|
20
|
-
"
|
|
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
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
27
26
|
},
|
|
28
27
|
"peerDependencies": {
|
|
29
|
-
"@alliance-droid/svelte-component-library": ">=
|
|
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.
|
|
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": "
|
|
56
|
+
"license": "MIT",
|
|
59
57
|
"publishConfig": {
|
|
60
58
|
"access": "restricted"
|
|
61
59
|
},
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/alliance-droid/status-feedback-system.git"
|
|
65
63
|
}
|
|
66
64
|
}
|