Per-row action bar on the list page:
[Edit] [Copy link] [Open] [Close|Reopen] [Delete] — Delete confirms then
DELETE + invalidateAll(); Close/Reopen PATCHes status, no confirm; per-row
error banner.
Full English rewrite of admin chrome (list + detail + builder), login,
landing. Drop dev jargon — "instance" / "slug" / "schema" / docs/plans
references gone. Sample SAMPLE_FORM content also translated to a
session-feedback example. Participant /f/<slug> stays untouched (author-
supplied content). Results.svelte stays as-is too — shared with the
participant page where the surrounding chrome is German.
Tab strip on /admin/feedback/<id> restyled as a segmented pill bar
(.fb-tabs / .fb-tab / .fb-tab--active). Active tab gets the green
primary-light background + bolder text + radius-md, hover lifts to white.
Earlier tabs were nearly invisible.
Split create form to its own route /admin/feedback/new (page + auth-only
+page.server.ts mirroring the list loader). List page now shows just the
form list with a "+ New form" CTA in the header.
- html/body reset (margin 0, bg + color via tokens, fill viewport) — kills
the white user-agent frame around the dark page in dark mode.
- Replace --fb-* tokens with the flexsiebels variables.css token set
(--color-*, --radius-*, --shadow-*) and keep --fb-* aliases pointing at
them so existing class names work without rewriting.
- Page background is now the flexsiebels green gradient (light mode) and
a charcoal→teal gradient (dark mode).
- Buttons: green primary with shadow + active-press transform, ghost
variant with proper border + hover.
- Inputs/textareas: rounded-md, focus ring via box-shadow on
--color-border-focus instead of bare outline.
- Scale buttons: hover hint + green active state with shadow.
- Chat posts + builder cards: white surface with shadow-sm.
- Banners: subtle elevation; dark-mode variants for closed/error so they
read on the dark gradient.
- Headings: tighter letter-spacing, slightly larger h1.
System dark mode (prefers-color-scheme) still toggles automatically; the
participant page sections stay flat (no re-introduced frame).
Migration: + fdbck.feedback_instances.live_results_enabled bool default false
+ fdbck.feedback_submissions.form_snapshot jsonb (frozen form per submission)
Schemas (moved $lib/server/schemas.ts → $lib/schemas.ts so the form-validation
Zod runtime can be reused on the client):
- form_definition.version: "0.YYMMDD" (today = 0.260505) with .b/.c suffix
for same-day re-edits when older snapshots already use that day
- live_results_enabled on Create + Update DTOs
Server:
- submit/+server: writes the parsed form_definition into form_snapshot so
results stay queryable after the form is later edited
- admin POST: stamps todayVersion() on first save
- admin PATCH: stampVersion() keeps current version while no submission has
it yet; otherwise advances to today (or .b/.c)
- new $lib/server/results.ts: pure aggregation + version helpers
(scale → histogram + mean, choices → counts + other_count for vanished
options, boolean → yes/no, text → list of free-text answers)
- new GET /api/public/feedback/<slug>/results: gated on live_results_enabled,
strips free-text answers (count-only) for participant-side display
- admin GET + page loader return aggregated results alongside submissions
UI:
- Results.svelte component (shared admin/participant) — CSS bar charts,
no external lib
- FormBuilder.svelte — add/remove/reorder/edit questions, type switch,
options/scale config; visual ↔ JSON toggle in admin Edit tab keeps both
views in sync
- admin detail: new "Ergebnisse" tab with version stamp, "live_results"
checkbox in Edit tab, info banner about version bumps when submissions exist
- /f/<slug>: after submit (and only if live_results_enabled), polls
/results every 5s and renders <Results /> below the form
- supabase-js with .schema('fdbck') returns JSONB columns as JSON-encoded
strings; getInstanceBySlug + getInstanceById + admin list now JSON.parse
via a shared parseFormDefinition helper, so FeedbackFormDefinitionSchema
sees an object and questions actually render.
- footer: 'flexsiebels.de · per-Link Feedback' → 'fdbck.msbls.de'.
- .fb-section: drop the white card frame (transparent bg, no border, no
border-radius) — sections now flow flat on the page.
- README.md: stack, run-locally, test/check/build, structure tree, data
model summary, anti-abuse layers, scope notes, issue origin pointer.
- docs/plans/feedback-feature.md: copied verbatim from flexsiebels for
self-containment (single source of truth in this repo from now on).
Mirrors msbls.de pattern, simplified (no mbrian-core submodule clone).
UID note: oven/bun:1-alpine has a built-in 'bun' user at UID/GID 1000 and
`addgroup -u 1000` on top of it breaks the build silently. mExDraw#14
(commit fc62b9c) lost ~4 weeks of Dokploy deploys to that. Comment in the
Dockerfile so the next person doesn't trip over the same.
Production build verified locally: vite build ✓ (4.08s).
Direct port from flexsiebels worktree. Imports getInstanceBySlug from
$lib/server/feedback (which uses fdb()) — schema rename happens at the
helper level, page code is identical.
Behaviour:
- LocalStorage: feedback:display_name (global) + feedback:session:<slug>
- 3s polling /posts?since=<latest_ts>; auto-scroll on new
- Hidden posts: '(Beitrag entfernt)' for others; own session sees body + note
- Honeypot 'company' input (CSS-hidden, aria-hidden)
- 423 → closed banner; 429 → rate-limit message; required-validation client+server
- noindex meta + no-referrer
- Question types: short_text, long_text, single_choice, multi_choice, scale, boolean
Root +layout.svelte already gives the naked shell (no sidebar/footer/bottom-nav)
so the +layout@.svelte reset trick is unnecessary here.
bun run check: 0 errors, 5 warnings (known false-positive 'data captured at
init' on $state — data from server load doesn't change client-side; same warning
pattern as flexsiebels).
Public (slug-gated, auto-allowlisted):
- GET /api/public/feedback/[slug] — instance config
- POST /api/public/feedback/[slug]/submit — form submission (honeypot, rate-limit, required-validation, 423 if closed)
- GET /api/public/feedback/[slug]/posts — chat polling (?since=, hides body of moderated posts)
- POST /api/public/feedback/[slug]/posts — new chat post (honeypot, rate-limit, 423 if closed)
Admin (requireAuth, owner-scoped):
- GET/POST /api/admin/feedback — list/create
- GET/PATCH/DELETE /api/admin/feedback/[id] — detail/update/delete (PATCH closes/reopens, sets closed_at)
- POST /api/admin/feedback/[id]/posts/[post_id]/hide — toggle hidden flag
- GET /api/admin/feedback/[id]/export?format=csv|json — single-file dump
Auth:
- POST /api/auth/sign-in — Supabase email+password, sets access+refresh cookies
- POST /api/auth/sign-out — clears cookies
bun run check: 0 errors, 0 warnings.
Bootstrap from /home/m/dev/web/msbls.de template:
- SvelteKit 2.15 + Svelte 5 + adapter-node + bun + vite 6
- Deps trimmed: @supabase/supabase-js, postgres, zod (+ dev: kit, vite-plugin-svelte, svelte-check, typescript)
- No mbrian-core submodule (irrelevant for fdbck)
- src/app.html minimal (no fonts, no theme toggler)
- src/app.d.ts declares App.Locals { userId: string | null }
- robots.txt Disallow: / (whole app is naked, per-link or auth-only)
- .env.example: Supabase + PUBLIC_SITE_URL + optional COOKIE_DOMAIN
Initial mai init scaffolding (.claude, .m, .mcp.json, AGENTS.md) bundled in
this first commit since the repo was empty before bootstrap.
Spawned from m/flexsiebels.de#63 pivot — see docs/plans/feedback-feature.md
for the full spec (copied in next commit).