The header was the densest surface in the app: 8 controls in a single
flex-wrap row plus a separate Share section bolted between header and tabs.
This commit collapses both into something readable.
Header:
- 8 visible controls → 3 visible. Status pill is now clickable and toggles
between open/closed (optimistic), replacing the Close/Reopen button. The
⋯ menu absorbs Copy /f/<slug>, Export CSV, Export JSON, and a separator
before Delete (still in red). All gone from the top-row strip: the raw
/f/slug, Copy link, Preview, Close/Reopen, CSV, JSON, Delete buttons.
- Quiet text-only .fb-back-link replaces the chip-style "← All forms"
button.
- New .fb-detail-head primitive lays out title-block on the left + actions
on the right with proper flex-wrap behaviour.
Share:
- Standalone <section data-fb-share> deleted. Its job moves to a new
inline .fb-share-strip directly under the title in the header.
- Strip always shows a usable URL: short_url if it exists, else the raw
/f/<slug>. Copy + Open ↗ buttons sit alongside.
- Below the strip, a compact <details class="fb-share-strip__replace">
holds the slug input + Create/Replace button. Summary text adapts to
whether a short link already exists.
Tab body:
- Drop the inner <h2> in every tab body (the active tab pill names the
section). All four tab bodies now use .fb-tab-body for consistent top
padding (var(--space-6)).
- "X responses already received…" warning becomes a muted .fb-question__help
line, not a .fb-banner box.
- Visual / JSON toggle becomes a real .fb-segment control matching the
shape of .fb-tabs (consistency).
- Save row uses .fb-save-row with the version pill ("Current version: vN")
rendered as a quiet .fb-version-note next to the Save button instead of
decorating the H2 like before.
- Submissions table extracted to a small <style> block (.fb-detail-table)
instead of inline style="..." chunks.
Click-outside-to-close + Escape close any open ⋯ menu, mirroring the list
page. Polling, refresh, and all backend contracts unchanged.
Delete still uses confirm() per m's override — deletion remains a deliberate
two-step action, no undo toast.
fdbck.msbls.de
Per-link feedback forms and live-chat masks. Anonymous, slug-gated, no auth required for participants.
Spun out from m/flexsiebels.de issue #63 — full design at docs/plans/feedback-feature.md.
Stack
SvelteKit 5 + Svelte 5 + bun + @sveltejs/adapter-node. Postgres + Supabase auth. Schema: fdbck.feedback_{instances,submissions,posts} on supa.flexsiebels.de (msupabase).
Run locally
cp .env.example .env # fill SUPABASE_*
bun install
bun run dev
Test + check
bun run test # rate-limit + public-scope unit tests
bun run check # svelte-check (type errors / a11y)
bun run build # adapter-node production build → ./build
Deploy
Dockerfile uses oven/bun:latest. Dokploy app: fdbck.msbls.de. DNS via Hostinger (handled out of band).
Structure
src/
hooks.server.ts — auth + public-scope policy gate
lib/server/
auth.ts — cookie JWT + Supabase refresh
fdb.ts — Postgres `fdbck` schema accessor
feedback.ts — slug generator + DB helpers + rate-limit constants
public-scope.ts — anonymous-DB-access fail-closed gate
rate-limit.ts — in-memory token bucket
schemas.ts — Zod request validation
supabase.ts — admin + anon client singletons
routes/
+page.svelte — landing
f/[slug]/ — public participant page (form + live chat)
admin/feedback/ — m's admin (list + detail + create)
api/
auth/ — sign-in / sign-out
public/feedback/ — anonymous slug-gated endpoints
admin/feedback/ — owner-scoped admin endpoints
Data model (canonical: design doc §5)
fdbck.feedback_instances— slug, title, description, owner_user_id, form_definition (jsonb), chat_enabled, status (open|closed), closed_atfdbck.feedback_submissions— instance_id, display_name (nullable = anonymous), client_session_id, answers (jsonb), client_ip, user_agentfdbck.feedback_posts— instance_id, display_name, client_session_id, body, hidden (m soft-moderate), client_ip, user_agent
Anti-abuse layers
- 32-char base62 slugs (~190 bits entropy)
- in-memory rate-limit (30 posts / 5 min, 10 submits / 5 min, per IP+slug)
- honeypot field on forms + chat (silently dropped)
- body length caps + closing kill-switch
- noindex meta +
robots.txtDisallow: /
Out of scope (v1)
Drag-drop form-builder · post reactions · realtime/SSE · CAPTCHA · trusted-tier owner sharing · branding/theming · auto-notifications. All have a clean upgrade path on the existing schema.
Issue origin
m/flexsiebels.de#63 — m PWA-voice 2026-05-05: "Im Wesentlichen quasi Microsoft Forms und Teams-Feedback in einem auf einer Webseite."