@alliance-droid/status-feedback-system 1.4.0 → 2.1.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - `userEmail` prop on `DevFeedback` is now **required** (was optional). Pass the authenticated user's email; pass an empty string when no user is signed in.
8
+ - Widget renders nothing (no bubble, no context menu) when `userEmail` is empty. The previous lock-screen UI for unauthenticated users is removed.
9
+ - Anonymous feedback submission is no longer possible through the widget.
10
+
11
+ ### Added
12
+
13
+ - Dev-mode console warning when `userEmail` is empty, to surface misconfigurations in consumer apps.
14
+
3
15
  ## 1.3.0
4
16
 
5
17
  - Add composable FeedbackBoard components (FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm)
@@ -11,7 +11,6 @@
11
11
  } from '@alliance-droid/svelte-component-library';
12
12
  import DevContextMenu from './dev/DevContextMenu.svelte';
13
13
  import DevOnboardingToast from './dev/DevOnboardingToast.svelte';
14
- import { captureScreenshot, type ScreenshotProvider } from './dev/screenshot.js';
15
14
  import {
16
15
  detectEnvironment,
17
16
  getEnvironmentName,
@@ -31,16 +30,19 @@
31
30
  showOnboarding?: boolean;
32
31
  /** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
33
32
  boardUrl?: string;
34
- /** Authenticated user email. Required for submitting feedback. */
35
- userEmail?: string;
36
33
  /**
37
- * Custom screenshot provider. When supplied, this is called instead of
38
- * the default html2canvas client-side capture. Use this to delegate
39
- * screenshot capture to a server endpoint (e.g. Puppeteer / screenshot-service).
34
+ * Authenticated user email. Required.
40
35
  *
41
- * See `createServerScreenshotProvider()` for a ready-made helper.
36
+ * Pass an empty string when the user is not signed in — the widget will
37
+ * render nothing (no floating bubble, no menu, no form). Anonymous
38
+ * feedback is not supported.
42
39
  */
43
- screenshotProvider?: ScreenshotProvider;
40
+ userEmail: string;
41
+ /**
42
+ * Endpoint on the consumer app that issues render tokens.
43
+ * Default: '/svc/internal/issue-render-token'
44
+ */
45
+ renderTokenEndpoint?: string;
44
46
  }
45
47
 
46
48
  let {
@@ -51,9 +53,24 @@
51
53
  showOnboarding = true,
52
54
  boardUrl,
53
55
  userEmail,
54
- screenshotProvider,
56
+ renderTokenEndpoint = '/svc/internal/issue-render-token',
55
57
  }: Props = $props();
56
58
 
59
+ // Dev-mode warning when userEmail is empty
60
+ $effect(() => {
61
+ if (!userEmail && typeof console !== 'undefined') {
62
+ // eslint-disable-next-line no-restricted-globals -- safe typeof guard for universal code
63
+ const proc = typeof globalThis !== 'undefined' ? (globalThis as Record<string, unknown>).process as { env?: Record<string, string> } | undefined : undefined;
64
+ const env = proc?.env?.NODE_ENV || 'development';
65
+ if (env !== 'production') {
66
+ console.warn(
67
+ '[status-feedback-system] <DevFeedback> was rendered with an empty userEmail. ' +
68
+ "The widget will not appear. Pass the authenticated user's email, or do not render <DevFeedback> when no user is signed in.",
69
+ );
70
+ }
71
+ }
72
+ });
73
+
57
74
  // --- Client (lazy, stable) ---
58
75
  let client: StatusClient | null = $state(null);
59
76
  let lastApiUrl = '';
@@ -70,6 +87,7 @@
70
87
 
71
88
  // --- Environment gate ---
72
89
  let isEnabled = $state(false);
90
+ let hasUserEmail = $derived(!!userEmail);
73
91
 
74
92
  // --- State ---
75
93
  let menuOpen = $state(false);
@@ -125,13 +143,13 @@
125
143
  let formTitle = $state('');
126
144
  let formMessage = $state('');
127
145
 
128
- let isAuthenticated = $derived(!!userEmail);
146
+
129
147
  let submitting = $state(false);
130
148
  let submitResult = $state<'success' | 'error' | null>(null);
131
149
 
132
- let screenshotData = $state<string | null>(null);
133
- let capturingScreenshot_ = $state(false);
134
- let screenshotFailed = $state(false);
150
+ let renderContext = $state<{ url: string; scrollX: number; scrollY: number; renderToken: string; issuer: string } | null>(null);
151
+ let fetchingRenderToken = $state(false);
152
+ let renderTokenFailed = $state(false);
135
153
 
136
154
  // Drag state
137
155
  let isDragging = $state(false);
@@ -171,7 +189,7 @@
171
189
  const ONBOARDING_KEY = 'sf-devfeedback-onboarded';
172
190
 
173
191
  onMount(() => {
174
- isEnabled = detectEnvironment(enabled);
192
+ isEnabled = detectEnvironment(enabled) && !!userEmail;
175
193
  if (!isEnabled) return;
176
194
 
177
195
  // Hook console.error
@@ -227,16 +245,36 @@
227
245
  if (e.key === 'Escape') {
228
246
  menuOpen = false;
229
247
  formOpen = false;
230
- screenshotData = null;
248
+ renderContext = null;
231
249
  }
232
250
  }
233
251
 
234
- async function doScreenshot() {
235
- capturingScreenshot_ = true;
236
- screenshotFailed = false;
237
- screenshotData = await captureScreenshot(screenshotProvider);
238
- if (!screenshotData) screenshotFailed = true;
239
- capturingScreenshot_ = false;
252
+ async function fetchRenderToken() {
253
+ fetchingRenderToken = true;
254
+ renderTokenFailed = false;
255
+ try {
256
+ const url = window.location.href;
257
+ const scrollX = Math.round(window.scrollX);
258
+ const scrollY = Math.round(window.scrollY);
259
+ const res = await fetch(renderTokenEndpoint, {
260
+ method: 'POST',
261
+ credentials: 'include',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify({ url, scrollX, scrollY }),
264
+ });
265
+ if (!res.ok) {
266
+ renderTokenFailed = true;
267
+ console.warn('[DevFeedback] Render token fetch failed:', res.status);
268
+ return;
269
+ }
270
+ const { token } = (await res.json()) as { token: string };
271
+ renderContext = { url, scrollX, scrollY, renderToken: token, issuer: new URL(url).origin };
272
+ } catch (err) {
273
+ renderTokenFailed = true;
274
+ console.warn('[DevFeedback] Render token fetch error:', err);
275
+ } finally {
276
+ fetchingRenderToken = false;
277
+ }
240
278
  }
241
279
 
242
280
  function openForm(type: 'bug' | 'feature') {
@@ -251,14 +289,14 @@
251
289
  modalY = 0;
252
290
 
253
291
  if (type === 'bug') {
254
- doScreenshot();
292
+ fetchRenderToken();
255
293
  } else {
256
- screenshotData = null;
294
+ renderContext = null;
257
295
  }
258
296
  }
259
297
 
260
298
  async function handleSubmit() {
261
- if (!formMessage.trim() || !isAuthenticated) return;
299
+ if (!formMessage.trim() || !hasUserEmail) return;
262
300
  submitting = true;
263
301
  submitResult = null;
264
302
 
@@ -280,8 +318,11 @@
280
318
  metadata.consoleErrors = [...consoleErrors];
281
319
  }
282
320
 
283
- if (screenshotData) {
284
- metadata.screenshot = screenshotData;
321
+ if (renderContext) {
322
+ metadata.renderToken = renderContext.renderToken;
323
+ metadata.issuer = renderContext.issuer;
324
+ metadata.scrollX = renderContext.scrollX;
325
+ metadata.scrollY = renderContext.scrollY;
285
326
  }
286
327
 
287
328
  const gitCommit = getEnvVar('VERCEL_GIT_COMMIT_SHA');
@@ -304,7 +345,7 @@
304
345
  setTimeout(() => {
305
346
  formOpen = false;
306
347
  submitResult = null;
307
- screenshotData = null;
348
+ renderContext = null;
308
349
  }, 2000);
309
350
  } catch {
310
351
  submitResult = 'error';
@@ -381,11 +422,6 @@
381
422
  {formType === 'bug' ? "We'll look into this." : "Thanks for the suggestion!"}
382
423
  </p>
383
424
  </div>
384
- {:else if !isAuthenticated}
385
- <div class="text-center py-6 text-foreground-tertiary">
386
- <i class="fa-solid fa-lock text-2xl" aria-hidden="true"></i>
387
- <p class="mt-2 text-sm">Sign in to submit feedback</p>
388
- </div>
389
425
  {:else}
390
426
  <div class="flex items-center gap-1.5 text-xs text-foreground-tertiary">
391
427
  <i class="fa-solid fa-user" aria-hidden="true"></i>
@@ -434,40 +470,30 @@
434
470
  {/if}
435
471
  </div>
436
472
 
437
- <!-- Screenshot preview -->
438
- {#if capturingScreenshot_}
473
+ <!-- Render token status -->
474
+ {#if fetchingRenderToken}
439
475
  <div class="flex items-center gap-2 text-sm text-foreground-tertiary">
440
- <Spinner /> Capturing screenshot...
476
+ <Spinner /> Preparing screenshot...
441
477
  </div>
442
- {:else if screenshotFailed}
443
- <div class="rounded-md border border-border bg-surface p-3">
444
- <div class="flex items-center gap-2 text-sm text-foreground-secondary mb-1">
445
- <i class="fa-solid fa-image-slash" aria-hidden="true"></i>
446
- <span>Screenshot capture failed</span>
447
- </div>
448
- <p class="text-xs text-foreground-tertiary mb-2">Check console for error. May need: <code class="bg-background px-1 rounded">npm install html2canvas</code></p>
449
- <button
450
- class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
451
- onclick={doScreenshot}
452
- >
453
- Retry
454
- </button>
455
- </div>
456
- {:else if screenshotData}
457
- <div class="relative">
458
- <img
459
- src={screenshotData}
460
- alt="Screenshot"
461
- class="w-full rounded-md border border-border max-h-40 object-cover object-top"
462
- />
478
+ {:else if renderTokenFailed}
479
+ <div class="rounded-md border border-border bg-surface p-3">
480
+ <div class="flex items-center gap-2 text-sm text-foreground-secondary mb-1">
481
+ <i class="fa-solid fa-image-slash" aria-hidden="true"></i>
482
+ <span>Screenshot setup failed</span>
483
+ </div>
484
+ <p class="text-xs text-foreground-tertiary mb-2">Your feedback will be submitted without a screenshot.</p>
463
485
  <button
464
- 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"
465
- onclick={() => (screenshotData = null)}
466
- aria-label="Remove screenshot"
486
+ class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
487
+ onclick={fetchRenderToken}
467
488
  >
468
- <i class="fa-solid fa-xmark"></i>
489
+ Retry
469
490
  </button>
470
491
  </div>
492
+ {:else if renderContext}
493
+ <div class="rounded-md border border-border bg-surface-secondary p-2 flex items-center gap-2 text-sm text-foreground-secondary">
494
+ <i class="fa-solid fa-camera" aria-hidden="true"></i>
495
+ <span>Screenshot will be captured server-side on submit</span>
496
+ </div>
471
497
  {/if}
472
498
  </div>
473
499
  {/if}
@@ -482,7 +508,7 @@
482
508
  </div>
483
509
 
484
510
  <!-- Footer -->
485
- {#if submitResult !== 'success' && isAuthenticated}
511
+ {#if submitResult !== 'success' && hasUserEmail}
486
512
  <div class="flex justify-end gap-2 px-5 py-4 border-t border-border shrink-0">
487
513
  <Button variant="neutral" style="outline" onclick={() => (formOpen = false)}>
488
514
  Cancel
@@ -1,4 +1,3 @@
1
- import { type ScreenshotProvider } from './dev/screenshot.js';
2
1
  interface Props {
3
2
  apiUrl: string;
4
3
  apiKey: string;
@@ -10,16 +9,19 @@ interface Props {
10
9
  showOnboarding?: boolean;
11
10
  /** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
12
11
  boardUrl?: string;
13
- /** Authenticated user email. Required for submitting feedback. */
14
- userEmail?: string;
15
12
  /**
16
- * Custom screenshot provider. When supplied, this is called instead of
17
- * the default html2canvas client-side capture. Use this to delegate
18
- * screenshot capture to a server endpoint (e.g. Puppeteer / screenshot-service).
13
+ * Authenticated user email. Required.
19
14
  *
20
- * See `createServerScreenshotProvider()` for a ready-made helper.
15
+ * Pass an empty string when the user is not signed in — the widget will
16
+ * render nothing (no floating bubble, no menu, no form). Anonymous
17
+ * feedback is not supported.
21
18
  */
22
- screenshotProvider?: ScreenshotProvider;
19
+ userEmail: string;
20
+ /**
21
+ * Endpoint on the consumer app that issues render tokens.
22
+ * Default: '/svc/internal/issue-render-token'
23
+ */
24
+ renderTokenEndpoint?: string;
23
25
  }
24
26
  declare const DevFeedback: import("svelte").Component<Props, {}, "">;
25
27
  type DevFeedback = ReturnType<typeof DevFeedback>;
package/dist/index.d.ts CHANGED
@@ -9,8 +9,6 @@
9
9
  */
10
10
  export type { Project, System, Incident, IncidentUpdate, Feedback, FeedbackResponse, FeedbackVisibility, StatusHistoryEntry, SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus, ApiResponse, } from './types.js';
11
11
  export { deriveOverallStatus } from './utils.js';
12
- export { createServerScreenshotProvider } from './components/dev/screenshot.js';
13
- export type { ScreenshotProvider } from './components/dev/screenshot.js';
14
12
  export { createStatusClient } from './api.js';
15
13
  export type { StatusClient, StatusClientConfig, StatusData, IncidentData, UptimeData, BoardData, BoardItem, BoardCategory } from './api.js';
16
14
  export { default as StatusPage } from './components/StatusPage.svelte';
package/dist/index.js CHANGED
@@ -9,7 +9,6 @@
9
9
  */
10
10
  // ── Utilities ──
11
11
  export { deriveOverallStatus } from './utils.js';
12
- export { createServerScreenshotProvider } from './components/dev/screenshot.js';
13
12
  // ── API Client ──
14
13
  export { createStatusClient } from './api.js';
15
14
  // ── Client Components ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/status-feedback-system",
3
- "version": "1.4.0",
3
+ "version": "2.1.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",
@@ -18,30 +18,32 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "build": "svelte-package -o dist && publint",
21
- "lint": "svelte-check --tsconfig ./tsconfig.json",
21
+ "lint": "eslint src",
22
22
  "check": "svelte-check --tsconfig ./tsconfig.json",
23
23
  "test": "vitest run",
24
24
  "test:watch": "vitest",
25
25
  "prepublishOnly": "npm run build"
26
26
  },
27
- "dependencies": {
28
- "html2canvas": "^1.4.1"
29
- },
30
27
  "peerDependencies": {
31
28
  "@alliance-droid/svelte-component-library": ">=1.0.0",
32
29
  "svelte": "^5.0.0"
33
30
  },
34
31
  "devDependencies": {
35
32
  "@alliance-droid/svelte-component-library": "^1.0.5",
33
+ "@eslint/js": "^10.0.1",
36
34
  "@sveltejs/package": "^2.3.0",
37
35
  "@sveltejs/vite-plugin-svelte": "^6.0.0",
38
36
  "@testing-library/jest-dom": "^6.9.1",
39
37
  "@testing-library/svelte": "^5.3.1",
38
+ "eslint": "^10.4.0",
39
+ "eslint-plugin-svelte": "^3.17.1",
40
+ "globals": "^17.6.0",
40
41
  "jsdom": "^29.0.0",
41
42
  "publint": "^0.3.0",
42
43
  "svelte": "^5.48.0",
43
44
  "svelte-check": "^4.3.0",
44
45
  "typescript": "^5.9.0",
46
+ "typescript-eslint": "^8.59.4",
45
47
  "vite": "^7.3.0",
46
48
  "vitest": "^4.0.0"
47
49
  },
@@ -61,4 +63,4 @@
61
63
  "type": "git",
62
64
  "url": "git+https://alliance.ghe.com/alliance/status-feedback-system.git"
63
65
  }
64
- }
66
+ }
@@ -1,36 +0,0 @@
1
- /**
2
- * Screenshot capture utility for DevFeedback.
3
- *
4
- * Supports two modes:
5
- * 1. Custom provider — consumer supplies a `ScreenshotProvider` callback
6
- * (e.g. server-side capture via Puppeteer / screenshot-service).
7
- * 2. html2canvas fallback — dynamically imports html2canvas if no provider
8
- * is given (html2canvas is an optional peer dependency).
9
- */
10
- /**
11
- * A function that captures a screenshot and returns a data URL (or null on failure).
12
- * Consumers can implement this however they like — server endpoint, different
13
- * client library, canvas API, etc.
14
- */
15
- export type ScreenshotProvider = () => Promise<string | null>;
16
- /**
17
- * Capture a screenshot using the given provider, or fall back to html2canvas.
18
- */
19
- export declare function captureScreenshot(provider?: ScreenshotProvider): Promise<string | null>;
20
- /**
21
- * Create a screenshot provider that POSTs to a server endpoint.
22
- *
23
- * The endpoint receives `{ url, viewport }` and should return
24
- * `{ screenshot: "data:image/..." }` (or any JSON with a `screenshot` string field).
25
- *
26
- * @example
27
- * ```ts
28
- * <DevFeedback
29
- * screenshotProvider={createServerScreenshotProvider('/api/screenshot')}
30
- * ...
31
- * />
32
- * ```
33
- */
34
- export declare function createServerScreenshotProvider(endpoint: string, options?: {
35
- headers?: Record<string, string>;
36
- }): ScreenshotProvider;
@@ -1,94 +0,0 @@
1
- /**
2
- * Screenshot capture utility for DevFeedback.
3
- *
4
- * Supports two modes:
5
- * 1. Custom provider — consumer supplies a `ScreenshotProvider` callback
6
- * (e.g. server-side capture via Puppeteer / screenshot-service).
7
- * 2. html2canvas fallback — dynamically imports html2canvas if no provider
8
- * is given (html2canvas is an optional peer dependency).
9
- */
10
- /**
11
- * Capture a screenshot using the given provider, or fall back to html2canvas.
12
- */
13
- export async function captureScreenshot(provider) {
14
- if (provider) {
15
- try {
16
- return await provider();
17
- }
18
- catch (err) {
19
- console.error('[DevFeedback] Screenshot provider failed:', err);
20
- return null;
21
- }
22
- }
23
- return captureWithHtml2Canvas();
24
- }
25
- /**
26
- * Create a screenshot provider that POSTs to a server endpoint.
27
- *
28
- * The endpoint receives `{ url, viewport }` and should return
29
- * `{ screenshot: "data:image/..." }` (or any JSON with a `screenshot` string field).
30
- *
31
- * @example
32
- * ```ts
33
- * <DevFeedback
34
- * screenshotProvider={createServerScreenshotProvider('/api/screenshot')}
35
- * ...
36
- * />
37
- * ```
38
- */
39
- export function createServerScreenshotProvider(endpoint, options) {
40
- return async () => {
41
- const res = await fetch(endpoint, {
42
- method: 'POST',
43
- headers: {
44
- 'Content-Type': 'application/json',
45
- ...options?.headers,
46
- },
47
- body: JSON.stringify({
48
- url: window.location.href,
49
- viewport: {
50
- width: window.innerWidth,
51
- height: window.innerHeight,
52
- },
53
- }),
54
- });
55
- if (!res.ok)
56
- return null;
57
- const data = await res.json();
58
- return data.screenshot ?? null;
59
- };
60
- }
61
- /** html2canvas fallback — gracefully degrades if not installed. */
62
- async function captureWithHtml2Canvas() {
63
- try {
64
- const html2canvas = (await import('html2canvas')).default;
65
- const canvas = await html2canvas(document.body, {
66
- logging: false,
67
- useCORS: true,
68
- scale: 1,
69
- width: window.innerWidth,
70
- height: window.innerHeight,
71
- windowWidth: window.innerWidth,
72
- windowHeight: window.innerHeight,
73
- ignoreElements: (element) => {
74
- // Skip elements that might have unsupported CSS (oklab, oklch, color-mix)
75
- const style = window.getComputedStyle(element);
76
- const hasUnsupportedColor = [
77
- style.color,
78
- style.backgroundColor,
79
- style.borderColor,
80
- ].some(color => color && (color.includes('oklab') || color.includes('oklch') || color.includes('color-mix')));
81
- return hasUnsupportedColor;
82
- },
83
- });
84
- return canvas.toDataURL('image/jpeg', 0.7);
85
- }
86
- catch (err) {
87
- console.error('[DevFeedback] Screenshot capture failed:', err);
88
- if (err instanceof Error && err.message.includes('oklab')) {
89
- console.warn('[DevFeedback] Your CSS uses oklab/oklch colors which html2canvas doesn\'t support. Screenshot skipped.');
90
- }
91
- // html2canvas failed (unsupported CSS, cross-origin iframe, etc.)
92
- return null;
93
- }
94
- }