@alliance-droid/status-feedback-system
v4.3.3
Published
Drop-in system status pages and user feedback forms for SvelteKit apps
Maintainers
Readme
@alliance-droid/status-feedback-system
Drop-in system status pages and user feedback components for SvelteKit apps.
This package provides UI components plus an optional SvelteKit server proxy for talking to a status-feedback-admin backend. The admin app handles data storage, API key management, and the admin dashboard.
Upgrading an existing app to 4.3? Read Upgrading to 4.3, especially if your proxy replaces a server-delegated placeholder with an access token. This guide is included in the installed package's README.md.
Architecture
┌─────────────────────────┐ ┌──────────────────────────┐
│ Your SvelteKit App │ │ status-feedback-admin │
│ │ │ │
│ ┌───────────────────┐ │ API │ ┌────────────────────┐ │
│ │ StatusPage │──┼───────┼──│ /api/status │ │
│ │ FeedbackButton │ │ │ │ /api/feedback │ │
│ │ DevFeedback │ │ │ │ /api/incidents │ │
│ │ Feedback board │ │ │ │ /api/board │ │
│ │ (composable) │ │ │ │ │ │
│ └───────────────────┘ │ │ └────────────────────┘ │
│ │ │ │ │
│ npm package │ │ MSSQL + Azure Blob │
└─────────────────────────┘ └──────────────────────────┘Setup
1. Deploy the admin backend
The admin app (status-feedback-admin) provides the API, database, and admin dashboard. Deploy it first.
Required env vars on the admin app:
MSSQL_CONNECTION_STRING=Server=...;Initial Catalog=StatusFeedback;...
SESSION_SECRET=your-secret
# Optional — stores screenshots in Azure Blob Storage instead of base64 in DB
AZURE_FEEDBACK_BLOB_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
AZURE_FEEDBACK_BLOB_CONTAINER=screenshots2. Create a project and keep the API key server-side
Log into the admin dashboard, create a project, and copy its API key (prefixed sf_). Store it in a private server-side environment variable, not a PUBLIC_ / browser-delivered variable.
3. Install the client package
npm install @alliance-droid/status-feedback-systemPeer dependencies: svelte ^5.55.7, @alliance-droid/svelte-component-library >=1.3.2
@alliance-droid/svelte-component-librarymust be>=1.3.2— earlier versions are incompatible with Svelte5.56+and throwtarget.exclude.has is not a functionwhen board components render.
4. Add the recommended SvelteKit proxy
Use one catch-all route in the consuming app. The package owns the admin API path mapping, request validation, key injection, and key query stripping; the app owns auth and app-specific policy.
// src/routes/svc/feedback/[...path]/+server.ts
import { env } from '$env/dynamic/private';
import { createSvelteKitFeedbackProxy } from '@alliance-droid/status-feedback-system/sveltekit/server';
export const { GET, POST, PATCH } = createSvelteKitFeedbackProxy({
apiUrl: env.SF_API_URL,
apiKey: env.SF_API_KEY,
resolveUser: async (event) => {
const user = event.locals.user;
return user ? { email: user.email, roles: user.roles } : null;
},
requireUser: true,
canSubmitInAppFeedback: ({ user }) =>
user.roles.includes('QA') || user.roles.includes('ProductOwner'),
});Then point components at the same-origin route and omit apiKey:
<DevFeedback apiUrl="/svc/feedback/dev" enabled={canSubmitFeedback} userEmail={user.email} />Direct admin calls with apiKey="sf_..." remain supported for existing integrations, but the proxy is the hardened default.
Upgrading to 4.3
Which consumers need a change?
Version 4.3 discovers management permissions on GET /api/board. Earlier versions probed an item's /transitions endpoint after loading the board. The board now needs an authenticated list response containing canManageStatus and per-item canChangeStatus / needsInput fields before showing editable controls or the Needs my input filter.
| Integration | Required action |
| --- | --- |
| createAuthTokenGetter() or another real access-token getter with the package's 4.3 proxy | No additional proxy change for token forwarding. The package forwards the token on board reads automatically. Check the backend and access prerequisites below. |
| Direct admin calls with a real access-token getter | No custom proxy change. The 4.3 client sends the token on board reads; backend and access prerequisites still apply. |
| A custom proxy that forwards bearer tokens only on /transitions and /status | Also forward the user's bearer token on GET /api/board, including filtered and paginated reads. |
| A server-delegated proxy, such as SkyBridgeSPCC, with getAuthToken: () => 'server-delegated' | Replace the placeholder with the signed-in user's real access token on GET /api/board too. Passing the placeholder upstream makes the board read-only. |
| No management token getter | The board remains read-only for status management. Configure authenticated token delivery to enable manager controls. |
Installing 4.3.1 provides these instructions; it does not update custom routes in consuming apps. Apps using the affected patterns must update their proxy and deploy that app change.
Fix a custom server-delegated proxy
In your app's catch-all feedback route (typically src/routes/svc/feedback/[...path]/+server.ts), extend the existing token-delegation wrapper to cover these requests:
| Request | Token handling |
| --- | --- |
| GET api/board | Attach the signed-in user's access token when available. Ordinary reads may continue without one; status=needs_input requires management access. |
| GET api/board/feedback/:id/transitions | Keep the existing authenticated transition lookup. |
| PATCH api/board/feedback/:id/status | Keep the existing authenticated mutation and same-origin protections. |
For a SkyBridgeSPCC-style wrapper, replace the status-only route predicate with:
import type { RequestEvent } from '@sveltejs/kit';
function statusTokenRequirement(event: RequestEvent): 'optional' | 'required' | null {
const method = event.request.method.toUpperCase();
const path = (event.params.path ?? '').replace(/^\/+|\/+$/g, '');
if (method === 'GET' && path === 'api/board') return 'optional';
if (
(method === 'GET' && /^api\/board\/feedback\/[^/]+\/transitions$/.test(path)) ||
(method === 'PATCH' && /^api\/board\/feedback\/[^/]+\/status$/.test(path))
) return 'required';
return null;
}At the start of the existing delegation wrapper, use the classification to select the token. The following is a replacement fragment: isSameOriginStatusRequest, forbiddenFeedbackResponse, resolveFeedbackUser, and mintStatusAccessToken are your app's existing helpers, not package exports.
const requirement = statusTokenRequirement(event);
if (!requirement) return event;
const sameOrigin = isSameOriginStatusRequest(event);
if (!sameOrigin && requirement === 'required') return forbiddenFeedbackResponse();
if (!resolveFeedbackUser(event)) return event;
const accessToken = sameOrigin ? await mintStatusAccessToken(event) : null;
const headers = new Headers(event.request.headers);
if (accessToken) {
headers.set('Authorization', `Bearer ${accessToken}`);
} else {
headers.delete('Authorization');
}
// Continue with the existing code that clones event.request using these headers
// and passes the prepared event to the package proxy handler.Ensure your exported GET handler runs this wrapper for board reads, as well as transitions. Keep the app's session and authorization checks. Your same-origin check must reject cross-site delegation; ordinary board reads without same-origin evidence can continue without a bearer token. Never fall back to the placeholder or a browser-supplied token when server delegation fails. The delegated token must represent the signed-in user, not an application client-credentials identity.
For a custom pass-through proxy, forward the real Authorization header instead of minting a token. Both patterns must preserve status=needs_input, category, sort, limit, and offset, return the capability fields unchanged, and avoid shared caching of authenticated board responses. The package's 4.3 proxy handles these details when your wrapper passes it the prepared event.
Backend and access prerequisites
- Apply the admin server's
20260908120000_response_sourcemigration before deploying the matching admin update. An older server does not return the new capability fields; upgrading the client alone cannot enable them. - Use Svelte
^5.55.7and component library>=1.3.2in the consuming app. - Request an Alliance access token with audience and scope
status-feedback-admin, a user subject (sub), client identity (client_id), expiration, and the intended role claims. An ID token or placeholder does not satisfy this contract. If scopes or refresh-token configuration changed, sign out and back in. - Deploy the admin server's consumer app authorization update and configure
FEEDBACK_PROJECT_CLIENTSon the feedback server once per app:{"feedback-project-id":["consumer-oauth-client-id"]}. Use the project ID from its dashboard URL and the exact OAuthclient_idissued for the consumer app. App Admin, Administrator, and ProductOwner roles (case-insensitive) then grant the restricted board decisions. Users never need to log into the feedback admin app, be provisioned there, or receive project assignments. The feedback dashboard remains a separate operator login. This authorization update adds no migration and requires no Identity Server code change or consumer package update if tokens already reach the backend.
Check missing controls after upgrading
Inspect the JSON returned by your app's board request, such as /svc/feedback/api/board?status=open:
| Result | What to check |
| --- | --- |
| data.canManageStatus is absent | The app may point at an older server, or its proxy may be dropping the new fields. Check the actual SF_API_URL deployment. |
| data.canManageStatus is false | Check token delivery, token scope/audience/expiry, the user's app role, and that the feedback server's FEEDBACK_PROJECT_CLIENTS binds this project to the token's exact client_id. Ordinary reads intentionally succeed even when optional authentication fails. |
| data.canManageStatus is true, but an item's canChangeStatus is false | That item's current workflow state has no consumer action available. Try an Open item to check the manager controls. |
| The Needs my input request returns 403 | The server did not authorize project management for that request. Check that the same token reaches filtered and paginated board reads. |
After updating the proxy and configuring the feedback server, reload the board as an app Admin or ProductOwner who has never logged into the feedback dashboard. An actionable Open item should show the purple status control; Needs my input should be available. Also check an ordinary viewer still gets a read-only status badge.
Components
StatusPage
Full status page with systems, incidents, and uptime history bars. Supports self-fetching (provide apiUrl; add apiKey only for direct admin calls) or pass-through (provide data directly).
<script>
import { StatusPage } from '@alliance-droid/status-feedback-system';
</script>
<!-- Self-fetching through the same-origin proxy -->
<StatusPage
projectName="My App"
apiUrl="/svc/feedback"
/>
<!-- Pass-through mode -->
<StatusPage
projectName="My App"
systems={data.systems}
incidents={data.incidents}
statusHistory={data.statusHistory}
showUptime={true}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| projectName | string | required | Display name in the header |
| apiUrl | string | — | Proxy URL or direct admin backend URL (self-fetching mode) |
| apiKey | string | — | Project API key; omit when using the same-origin proxy |
| systems | System[] | — | Pass-through: system data |
| incidents | Incident[] | — | Pass-through: incident data |
| statusHistory | Record<string, StatusHistoryEntry[]> | — | Pass-through: uptime data |
| showUptime | boolean | true | Show 90-day uptime bars |
StatusBanner
Compact inline status indicator — good for footers or nav bars.
<StatusBanner
apiUrl="/svc/feedback"
statusUrl="/status"
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| systems | System[] | — | Pass-through mode |
| apiUrl | string | — | Proxy URL or direct admin backend URL |
| apiKey | string | — | Project API key; omit when using the same-origin proxy |
| statusUrl | string | — | Links to full status page (renders as <a>) |
FeedbackButton
Floating action button with an embedded feedback form. Fixed position, bottom corner.
<FeedbackButton
apiUrl="/svc/feedback"
position="bottom-right"
categories={['Bug', 'Feature Request', 'General']}
userEmail={currentUser?.email}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| apiUrl | string | required | Proxy URL or direct admin backend URL |
| apiKey | string | — | Project API key; omit when using the same-origin proxy |
| categories | string[] | ['Bug', 'Feature Request', 'General'] | Category options |
| userEmail | string | — | Pre-fill email field |
| position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Button position |
FeedbackForm
Standalone feedback form — embed it wherever you need it.
<FeedbackForm
apiUrl="/svc/feedback"
categories={['Bug', 'Feature Request']}
userEmail={currentUser?.email}
onSuccess={() => toast('Thanks!')}
onError={(msg) => toast(msg, 'error')}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| apiUrl | string | required | Proxy URL or direct admin backend URL |
| apiKey | string | — | Project API key; omit when using the same-origin proxy |
| categories | string[] | ['Bug', 'Feature Request', 'General'] | Category options |
| userEmail | string | — | Pre-fill email field |
| onSuccess | () => void | — | Called after successful submission |
| onError | (error: string) => void | — | Called on submission error |
Feedback board (composable)
📖 Full integration guide: docs/custom-feedback-integration.md — composing the parts into your own shell, status management via
getAuthToken, list restyling, and the not-configured fallback.
The board is composed from parts around one shared state (createFeedbackBoard), so it drops into your own layout/shell — app rails, breadcrumb, a right-hand detail panel, whatever you have.
<script lang="ts">
import {
createFeedbackBoard,
createAuthTokenGetter,
FeedbackSidebar,
FeedbackList,
FeedbackDetail,
FeedbackSubmitForm,
type BoardItem,
} from '@alliance-droid/status-feedback-system';
let { data } = $props();
const board = createFeedbackBoard({
apiUrl: '/svc/feedback',
userEmail: data.user?.email,
getAuthToken: createAuthTokenGetter(), // optional — enables status management
categoryConfig: {
Bug: { icon: 'fa-solid fa-bug', label: 'Bugs' },
'Feature Request': { icon: 'fa-solid fa-lightbulb', label: 'Ideas' },
},
});
let selected = $state<BoardItem | null>(null);
</script>
<YourSidebar><FeedbackSidebar {board} /></YourSidebar>
<YourMain><FeedbackList {board} onSelect={(i) => (selected = i)} /></YourMain>
<YourRail>
{#if selected}<FeedbackDetail {board} item={selected} />{/if}
</YourRail>
<!-- Render <FeedbackSubmitForm {board} /> wherever you want submission. -->createFeedbackBoard(options) returns a reactive FeedbackBoardState (items, categories, filters, voting, loadTransitions/setStatus, …) shared by every sub-component.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiUrl | string | required | Proxy URL or direct admin backend URL |
| apiKey | string | — | Project API key; omit when using the same-origin proxy |
| userEmail | string | — | Authenticated user (required to submit feedback/replies) |
| categoryConfig | Record<string, { label?, icon?, description? }> | {} | Relabel / add icons to sidebar categories |
| getAuthToken | () => string \| Promise<string \| null> | — | Bearer token so privileged users can set status (see below) |
| pageSize | number | 50 | Items per page |
Exported parts: createFeedbackBoard, FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm.
Categories are fixed by the system (Bug, Feature Request) — categoryConfig only relabels or adds icons.
Setting status (ProductOwners and Admins)
When you pass getAuthToken, Admins and ProductOwners of the app configured for the feedback project can accept feedback, sign off QA / UAT → Ready for Production, reopen it, or decline it (Won't do, Duplicate, Can't reproduce), where the current status permits the decision. No feedback-admin login or per-user assignment is needed. Reopens and declines require a reason. Only acceptance starts automation; engineering, release, and completion transitions remain in the admin app.
Editable items have purple status controls, while rows keep the consuming app's normal border color. A Needs your input cue appears only when a manager's action is required. The sidebar's Needs my input filter shows Open, Considering, Waiting on reply, and QA / UAT, filtered on the server across all pages. Everyone continues to see each item's granular status.
Deploy the matching admin-server capabilities update first. The packaged proxy forwards the optional board bearer token; custom proxies must also forward Authorization on board reads and preserve status=needs_input. Custom lists can use the backend's per-item canChangeStatus and needsInput fields with board.canManageStatus.
getAuthTokenmust return the user's Alliance OIDC access token whose audience coversstatus-feedback-admin(an id token, whoseaudis your app's client id, is rejected). It's called on demand; returnnullfor anonymous / non-privileged users and the control simply won't appear — the badge stays a plain read-only badge.- Use the built-in
createAuthTokenGetter()for this — it fetches a same-origin token endpoint (/auth/access-tokenby default), caches until just before expiry, single-flights, and returnsnullon any failure. With@alliance-droid/svelte-auth-core, setenableAccessTokenEndpoint: trueand request thestatus-feedback-adminscope so that endpoint exists. Create one instance at module level:export const getAuthToken = createAuthTokenGetter(); - The token travels as
Authorization: Bearer …to the admin backend, which verifies it against the IdP and derives the user's app roles. Authorization (app Admin/Administrator/ProductOwner role + the project's configured OAuth client binding + the restricted board transitions, including reason prompts on declines/reopens) is enforced server-side — the client never decides permissions. The backend returns acanManageflag (HTTP 200, not 403) so ordinary signed-in users get no console noise. - The available transitions are returned by the backend per item, so the menu always reflects exactly what that user may do.
Security note: use the SvelteKit proxy to keep the project
apiKeyout of browser-delivered code. Direct admin calls with an embedded key are still supported for existing integrations, but same-origin proxy mode is the recommended default.
DevFeedback
Feedback overlay for authorized users. Activated via Ctrl+right-click (Cmd+right-click on macOS) anywhere on the page.
Auto-captures: screenshots (via html2canvas), console errors, viewport size, current route, clicked element selector, and deployment context (Vercel env vars).
Gating: the widget shows only when enabled is true and userEmail is non-empty. enabled is the single gate — wire it to your own rule, typically a role/permission check. There is no environment auto-detection. See docs/role-based-access.md for the recommended role-based setup and the pitfalls.
<script>
import { DevFeedback } from '@alliance-droid/status-feedback-system';
</script>
<!-- In your root +layout.svelte -->
<DevFeedback
apiUrl="/svc/feedback/dev"
boardUrl="/feedback"
userEmail={user?.email ?? ''}
enabled={userCanGiveFeedback}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| apiUrl | string | required | Proxy URL or direct admin backend URL. Use /svc/feedback/dev with the package proxy for stricter widget policy. |
| apiKey | string | — | Project API key; omit when using the same-origin proxy |
| enabled | boolean | required | The only gate — wire to your role/permission check |
| userEmail | string | required | Signed-in user's email. Empty string ⇒ renders nothing (anonymous feedback unsupported) |
| boardUrl | string | — | URL for "View Open Issues" link. Hidden if omitted. |
| maxConsoleErrors | number | 20 | Max console errors to capture |
| showOnboarding | boolean | true | Show "Ctrl+right-click" toast on first visit |
| renderTokenEndpoint | string | /svc/internal/issue-render-token | App endpoint that mints render tokens for screenshot capture |
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.
API Client
All components use a shared API client. You can also use it directly:
import { createStatusClient } from '@alliance-droid/status-feedback-system';
const client = createStatusClient({
apiUrl: '/svc/feedback',
fetch: event.fetch, // Optional: pass SvelteKit's fetch for SSR
});
const { systems, overallStatus } = await client.getStatus();
const { incidents } = await client.getIncidents({ limit: 5, includeResolved: true });
const { history } = await client.getUptime({ days: 90 });
const feedback = await client.submitFeedback({ message: 'Great product!' });
const { items, total, categories } = await client.getBoard({ sort: 'votes', status: 'open' });Shared Components
Lower-level building blocks used by StatusPage — exported for custom layouts:
import {
StatusIndicator, // Status dot + label (operational/degraded/outage/maintenance)
UptimeBar, // 90-day uptime bar chart
IncidentTimeline, // Chronological incident updates
SystemCard, // Single system status card with uptime bar
} from '@alliance-droid/status-feedback-system';All Exports
// Components
import {
StatusPage, StatusBanner, FeedbackForm, FeedbackButton, DevFeedback,
StatusIndicator, UptimeBar, IncidentTimeline, SystemCard, PasteImageInput,
} from '@alliance-droid/status-feedback-system';
// Feedback board (composable)
import {
createFeedbackBoard,
createAuthTokenGetter,
FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm,
} from '@alliance-droid/status-feedback-system';
import type {
FeedbackBoardState, FeedbackBoardOptions, CategoryConfig,
AuthTokenGetter, AuthTokenGetterOptions,
} from '@alliance-droid/status-feedback-system';
// API Client
import { createStatusClient } from '@alliance-droid/status-feedback-system';
import type {
StatusClient, StatusClientConfig,
StatusData, IncidentData, UptimeData, BoardData, BoardItem, BoardCategory,
} from '@alliance-droid/status-feedback-system';
// SvelteKit server proxy
import {
createSvelteKitFeedbackProxy,
isFeedbackProxyConfigured,
} from '@alliance-droid/status-feedback-system/sveltekit/server';
// Utilities
import { deriveOverallStatus } from '@alliance-droid/status-feedback-system';
// Types
import type {
Project, System, Incident, IncidentUpdate, Feedback, FeedbackResponse,
FeedbackVisibility, StatusHistoryEntry, ApiResponse,
SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus,
} from '@alliance-droid/status-feedback-system';License
Copyright © 2026 Alliance Technical Group. All rights reserved.
