m: "The difference between our form and the live chat needs to be more
prominent. I think we should style it more like a chat, too — maybe
separate into two columns — Form on the Left, Chat on the Right. And
chat messages more... like a chat on the phone. And we colorize different
users differently."
Layout
- New .fb-shell--wide modifier (max-width 960px) on the participant
shell so the two columns breathe. .fb-participant grid: 1fr at <900px,
1fr 1fr at ≥900px. Form column on the left, chat column on the right.
Single-column flow (form-only or chat-only) just fills its column.
- Title + description + closed-line + name field stay full-width above
the columns. Live results + footer stay full-width below.
Chat bubbles
- New .fb-bubble primitive replaces the old .fb-chat__post box-with-border
pattern on /f/[slug] only (admin moderator UI keeps .fb-chat__post).
- My posts: right-aligned, --color-primary background, white text, no
border. Others: left-aligned, --color-bg-secondary background, 3px
left-border in the speaker's deterministic color.
- Author name: tiny + weight 500. For others, name text is the speaker's
color; for me, it's --color-primary (you stay green).
- Timestamp: HH:mm if today, dd.MM HH:mm otherwise; viewer-localised.
- Vertical density: 0.6rem gap by default, +0.5rem extra when the speaker
changes (bumps to ~1.1rem — visually distinct turn boundaries).
Per-user color
- 7-color palette (rose / orange / amber / cyan / blue / violet / pink),
greens deliberately excluded so they don't clash with --color-primary
which the viewer's own bubbles use.
- Stable hash of client_session_id maps to a palette index — same user
gets the same color across reloads.
- Color applied to: author name text, bubble left-border. No avatar
circle in v1.
Compose
- New .fb-compose at the bottom of the chat column (in flex-flow, no
position:sticky needed — bubbles take flex:1 and scroll, compose stays
pinned). Textarea + send button side-by-side.
- Single-row textarea (rows=1, max-height 10rem) grows on input.
- Enter alone sends; Shift+Enter inserts a newline; IME composition is
respected (e.zh ne IME's confirm Enter doesn't submit).
- Placeholder includes the keyboard hint in German.
Auto-scroll
- $effect on posts.length scrolls the bubbles container to the bottom
on append + initial load with behavior:'smooth'. Replaces the old
imperative queueMicrotask calls inside fetchPosts/postChat.
Form column
- Section H2 ("Fragebogen") dropped — column boundary already names the
section. Submit button: full-width on mobile, auto-width on desktop.
- Submit-success banner now uses .fb-form-banner--success class
(replaces inline style hardcoded green).
CSS hygiene
- Removed orphaned .fb-chat__list / .fb-chat__form / .fb-chat__form-actions
/ .fb-chat__empty (only the participant page used them; new bubble
primitives replace them). Admin-side .fb-chat / .fb-chat__post / etc.
kept intact for the moderator UI.
Anti-scope honored: admin pages untouched, German strings unchanged,
3s polling interval kept, no new dependencies.
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."