README + design doc copy
- 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).
This commit is contained in:
74
README.md
Normal file
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 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`](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
|
||||
|
||||
```sh
|
||||
cp .env.example .env # fill SUPABASE_*
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Test + check
|
||||
|
||||
```sh
|
||||
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_at
|
||||
- `fdbck.feedback_submissions` — instance_id, display_name (nullable = anonymous), client_session_id, answers (jsonb), client_ip, user_agent
|
||||
- `fdbck.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.txt` Disallow: /
|
||||
|
||||
## 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."*
|
||||
324
docs/plans/feedback-feature.md
Normal file
324
docs/plans/feedback-feature.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# Feedback Feature — Forms + Live-Chat unter flexsiebels.de
|
||||
|
||||
**Status**: design / awaiting m's go-no-go gate
|
||||
**Issue**: m/flexsiebels.de#63
|
||||
**Inventor**: knuth (shift-1, 2026-05-05)
|
||||
**Trigger**: HL UPC-Schulungen (28.05. Fristen, 25.06. Kosten), Talks, Workshops, mAi-Demos
|
||||
|
||||
---
|
||||
|
||||
## 1. Wunsch (verbatim, m, PWA-voice 2026-05-05 09:52)
|
||||
|
||||
> *"Ich brauche eine Feedback-Webseite, die mir erlaubt, Formulare, aber auch so eine Live-Chat-Maske zur Verfügung zu stellen. Die Nutzer sollen sich einen Namen geben können, wenn sie wollen, aber ansonsten auch anonym bleiben. Im Wesentlichen quasi Microsoft Forms und Teams-Feedback in einem auf einer Webseite. Und wir machen die grundsätzlich nicht authentifiziert, nur eben mit langen Links — Security by Obscurity."*
|
||||
|
||||
---
|
||||
|
||||
## 2. Stack-Reality-Check (vor Design)
|
||||
|
||||
Issue + `.claude/CLAUDE.md` referenzieren noch Deno Fresh + Preact. **Falsch.** Live-Stack ist:
|
||||
|
||||
- **SvelteKit 2.15 + Svelte 5** (runes), Bun, Vite 6, `@sveltejs/adapter-node`
|
||||
- **Supabase** mit getrennten Schemas: `flexsiebels.*` (App-Tabellen, helper `flex()`), `mbrian.*` (Graph, helper `mb()`), `public.*` (Infra)
|
||||
- **Auth**: Cookie-JWT oder API-Key, Hook in `src/hooks.server.ts` setzt `locals.userId`. Anonyme Requests an `/api/*` werden vom **public-scope policy gate** (issue #59) per Default zu 401 — Ausnahmen: `/api/public/*`, `/api/share/*`, `/api/auth/*`, `/api/gotify-public/*`. Diese Liste werden wir um den Feedback-Pfad erweitern.
|
||||
- **Migrations** in `website/migrations/` als nummerierte SQL-Dateien, **manuell** auf supa.flexsiebels.de via Supabase Studio (kein Auto-Runner).
|
||||
- **Validation**: Zod-Schemas in `src/lib/server/schemas.ts`.
|
||||
|
||||
`.claude/CLAUDE.md` ist stale — separater Cleanup-Issue empfohlen, nicht Teil dieses Tickets.
|
||||
|
||||
---
|
||||
|
||||
## 3. Scope v1
|
||||
|
||||
### In
|
||||
|
||||
- Pro Feedback-Event eine **Instance**, erreichbar über langen URL-Slug `/f/<32-base62>`.
|
||||
- Instance-Konfig kann **Form-Modus, Chat-Modus oder beides parallel** liefern (Datenmodell trägt beides, UI rendert was konfiguriert ist).
|
||||
- **Form-Felder**: short_text, long_text, single_choice, multi_choice, scale, boolean. Definition als JSON in m's Admin-View (kein drag-drop-Builder in v1).
|
||||
- **Live-Chat**: kurze Posts in Liste, optional displayed_name, **Polling** alle 3s (kein SSE/Realtime in v1).
|
||||
- **Anti-Abuse**: Per-IP-Rate-Limit (in-memory bucket), Body-Length-Caps, Honeypot-Feld, Closing-Switch.
|
||||
- **m-Admin** unter `/admin/feedback`: Liste aller Instances, Detail-View mit live-Chat + Submissions, Hide-Post-Button, Close-Button, **CSV/JSON-Export**.
|
||||
- **Persistenz Teilnehmer**: LocalStorage trägt `display_name` + `client_session_id` (UUID), Wieder-Aufruf zeigt eigene Posts hervorgehoben + Name vorausgefüllt.
|
||||
- **Closing**: Instance-Status `open` | `closed`. Closed → POST-Endpunkte 423, Read funktioniert weiter.
|
||||
- **noindex** für `/f/*` (robots.txt + meta tag).
|
||||
|
||||
### Out (v1)
|
||||
|
||||
- Drag-Drop Form-Builder (JSON-Editor reicht — m ist technisch)
|
||||
- Reaktionen (👍 etc.) auf Live-Posts
|
||||
- Branding/Theming pro Instance
|
||||
- Trusted-Tier Auth-Integration (Track D, separat)
|
||||
- Multi-Page-Forms, Logik-Branching, File-Upload
|
||||
- A/B-Testing, Cross-Instance-Stats
|
||||
- Real-Time via Supabase Realtime (Polling reicht; Migration zu Realtime trivial wenn nötig)
|
||||
- CAPTCHA / Turnstile (Honeypot + Rate-Limit + Kill-Switch reichen für v1; Eskalation später möglich)
|
||||
|
||||
---
|
||||
|
||||
## 4. URL-Schema
|
||||
|
||||
| Pfad | Zweck | Auth |
|
||||
|---|---|---|
|
||||
| `/f/<slug>` | Public participant page (form + chat) | none |
|
||||
| `/admin/feedback` | m's overview list | required |
|
||||
| `/admin/feedback/<id>` | Instance detail (live + submissions + moderate + export) | required |
|
||||
| `/api/public/feedback/<slug>` | GET instance config (form schema, chat_enabled, status) | none |
|
||||
| `/api/public/feedback/<slug>/submit` | POST form submission | none |
|
||||
| `/api/public/feedback/<slug>/posts?since=<ts>` | GET chat posts since timestamp | none |
|
||||
| `/api/public/feedback/<slug>/posts` | POST new chat post | none |
|
||||
| `/api/admin/feedback` | POST create / GET list | required |
|
||||
| `/api/admin/feedback/<id>` | PATCH / DELETE | required |
|
||||
| `/api/admin/feedback/<id>/posts/<post_id>/hide` | POST toggle hidden | required |
|
||||
| `/api/admin/feedback/<id>/export?format=csv\|json` | GET export | required |
|
||||
|
||||
**Slug-Entropy**: 32 chars base62 ≈ 190 bits. Brute-Force-resistent über HTTP. Optional via shlink (`/api/share`) verkürzbar wenn m verbal teilt.
|
||||
|
||||
**Conventions-Check**: passt zu `getCSSForRoute`-Pattern und `routes/api/<resource>` REST-Style.
|
||||
|
||||
---
|
||||
|
||||
## 5. Datenmodell (Schema `flexsiebels`)
|
||||
|
||||
```sql
|
||||
-- One feedback "event" — form, chat, or both
|
||||
CREATE TABLE flexsiebels.feedback_instances (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
slug TEXT NOT NULL UNIQUE, -- 32-char base62
|
||||
title TEXT NOT NULL, -- m-only label, e.g. "HL UPC 28.05."
|
||||
description TEXT, -- shown to participants on landing
|
||||
owner_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
form_definition JSONB, -- { questions: [...] } | NULL if no form
|
||||
chat_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
closed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (status IN ('open','closed')),
|
||||
CHECK (form_definition IS NOT NULL OR chat_enabled = true)
|
||||
);
|
||||
CREATE INDEX feedback_instances_owner_idx
|
||||
ON flexsiebels.feedback_instances(owner_user_id, created_at DESC);
|
||||
CREATE INDEX feedback_instances_slug_idx
|
||||
ON flexsiebels.feedback_instances(slug);
|
||||
|
||||
-- Form submissions (one row per submit)
|
||||
CREATE TABLE flexsiebels.feedback_submissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
instance_id UUID NOT NULL REFERENCES flexsiebels.feedback_instances(id) ON DELETE CASCADE,
|
||||
display_name TEXT, -- nullable = anonymous
|
||||
client_session_id TEXT NOT NULL, -- LocalStorage UUID
|
||||
answers JSONB NOT NULL, -- { question_id: value }
|
||||
client_ip INET, -- best-effort, abuse forensics
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX feedback_submissions_instance_idx
|
||||
ON flexsiebels.feedback_submissions(instance_id, created_at DESC);
|
||||
|
||||
-- Live chat posts
|
||||
CREATE TABLE flexsiebels.feedback_posts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
instance_id UUID NOT NULL REFERENCES flexsiebels.feedback_instances(id) ON DELETE CASCADE,
|
||||
display_name TEXT,
|
||||
client_session_id TEXT NOT NULL,
|
||||
body TEXT NOT NULL CHECK (length(body) BETWEEN 1 AND 2000),
|
||||
hidden BOOLEAN NOT NULL DEFAULT false, -- m soft-moderates
|
||||
client_ip INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX feedback_posts_instance_idx
|
||||
ON flexsiebels.feedback_posts(instance_id, created_at);
|
||||
```
|
||||
|
||||
**Migration-File**: `website/migrations/20260505_create_feedback_tables.sql`.
|
||||
Wie alle flexsiebels-Migrations: **manuell** als `supabase_admin` in Supabase Studio anwenden (siehe `20260504_move_to_flexsiebels_schema.sql` Header für die Begründung).
|
||||
|
||||
**RLS-Strategie**: Tabellen schreibt nur die App via `flex()` (service role, RLS bypass). Public-API-Handler validieren slug-basierten Zugriff selbst — keine RLS-Policies nötig (würde Anon-Zugang sowieso nicht durch Slug autorisieren). Konsistent mit dem Pattern der übrigen `flexsiebels.*`-Tabellen.
|
||||
|
||||
**Form-Definition JSON-Shape** (validiert per Zod):
|
||||
|
||||
```ts
|
||||
type Question =
|
||||
| { id: string; type: 'short_text'; label: string; required?: boolean; help?: string; placeholder?: string }
|
||||
| { id: string; type: 'long_text'; label: string; required?: boolean; help?: string; placeholder?: string }
|
||||
| { id: string; type: 'single_choice';label: string; required?: boolean; help?: string; options: string[] }
|
||||
| { id: string; type: 'multi_choice'; label: string; required?: boolean; help?: string; options: string[] }
|
||||
| { id: string; type: 'scale'; label: string; required?: boolean; help?: string; min: number; max: number; min_label?: string; max_label?: string }
|
||||
| { id: string; type: 'boolean'; label: string; required?: boolean; help?: string };
|
||||
|
||||
type FormDefinition = {
|
||||
intro?: string; // markdown allowed (sanitized via DOMPurify)
|
||||
outro?: string; // shown after submit
|
||||
questions: Question[];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend-Skizze
|
||||
|
||||
### `/f/<slug>` — Participant Page (mobile-first)
|
||||
|
||||
Reihenfolge auf der Seite:
|
||||
|
||||
1. **Header**: instance.title + (optional) instance.description
|
||||
2. **Optional**: "Dein Name (optional)" Input — leer = anonym, gespeichert in LocalStorage
|
||||
3. **Wenn `chat_enabled`**: Live-Chat-Maske
|
||||
- Posts-Liste, neueste oben oder unten? → **unten**, neueste unten (Teams-Stil, scrollt mit)
|
||||
- Eigene Posts hervorgehoben (matched via `client_session_id`)
|
||||
- Hidden Posts erscheinen für andere als "Beitrag entfernt", für eigenen Verfasser unverändert
|
||||
- Eingabefeld + Send-Button
|
||||
- Polling alle 3s mit `?since=<latest_ts>` (Long-Poll-light: nur Delta)
|
||||
4. **Wenn `form_definition`**: Form rendern
|
||||
- Sequentielle Felder, Submit-Button am Ende
|
||||
- Bei `required` Validation, sonst frei
|
||||
- Nach erfolgreichem Submit: Outro-Text + "Noch eine Antwort senden"-Link
|
||||
|
||||
Wenn `status='closed'`:
|
||||
- Banner "Diese Feedback-Sitzung ist geschlossen."
|
||||
- Form readonly oder versteckt
|
||||
- Chat readonly (Posts sichtbar, Eingabe weg)
|
||||
|
||||
**SEO**: `<svelte:head><meta name="robots" content="noindex,nofollow"/></svelte:head>`. `/f/*` zusätzlich in `static/robots.txt` als `Disallow: /f/`.
|
||||
|
||||
### `/admin/feedback` — m's Admin
|
||||
|
||||
- Liste aller Instances: Title, Slug (mit Copy-Button), Mode (Form/Chat/Beides), Status, Counts (#submissions, #posts), Created
|
||||
- "Neue Instance" → Formular: Title, Description, Form-Definition (JSON-Textarea mit Live-Validation), Chat-Enabled-Checkbox
|
||||
|
||||
### `/admin/feedback/<id>` — m's Detail
|
||||
|
||||
- Header: Title, Slug-Link, Status mit Close/Reopen-Button
|
||||
- Tabs: **Live Chat** | **Form Submissions**
|
||||
- Live-Chat-Tab: alle Posts inkl. hidden (markiert), pro Post Hide/Unhide-Button
|
||||
- Submissions-Tab: Tabellen-View aller Form-Antworten + per-row Detail
|
||||
- Export-Buttons: CSV (separates File für Submissions + Posts) + JSON (kompletter Dump)
|
||||
- Edit-Button für Form-Definition (nur erlaubt solange keine Submissions vorhanden, sonst Warnung — Schema-Drift)
|
||||
|
||||
---
|
||||
|
||||
## 7. Anti-Abuse-Strategie
|
||||
|
||||
| Layer | Mechanismus | Default |
|
||||
|---|---|---|
|
||||
| **Body size** | Length-Caps in Zod-Schema + DB-Constraint | post ≤ 2000 chars, long_text ≤ 5000, single field labels ≤ 200 |
|
||||
| **Per-IP rate** | In-memory token bucket pro Pfad | 30 posts / 5 min, 10 submits / 5 min, GET unlimitiert |
|
||||
| **Honeypot** | CSS-hidden field "company" — wenn gefüllt: 200 OK aber kein Insert | always on |
|
||||
| **Slug-Entropy** | 190 bits — Discovery via Brute-Force unrealistisch | enforced |
|
||||
| **Closing-Switch** | m setzt `status='closed'` → POST returns 423 | manual |
|
||||
| **Display-Name length** | ≤ 80 chars, no newlines | enforced |
|
||||
| **noindex** | `<meta>` + robots.txt | always on |
|
||||
|
||||
In-Memory Rate-Limit lebt im SvelteKit-Node-Prozess (`Map<string, { count, resetAt }>`). Ausreichend für v1 — bei Scale-Out-Bedarf später Redis.
|
||||
|
||||
**Was wir bewusst NICHT machen v1**: Cloudflare Turnstile (zusätzliches Account-Setup, JS-Bundle), CAPTCHA, IP-Blocking-Tabelle. Wenn wir gespammt werden: Issue → Turnstile in 1 Iteration nachrüstbar.
|
||||
|
||||
---
|
||||
|
||||
## 8. Technische Entscheidungen + Trade-Offs
|
||||
|
||||
### Polling vs. Realtime
|
||||
|
||||
Polling alle 3s gewählt:
|
||||
- + Trivial zu debuggen, funktioniert hinter jedem Reverse-Proxy, kein WebSocket-Stack
|
||||
- + Adapter-node + Traefik-Setup unverändert
|
||||
- − Bei vielen aktiven Teilnehmern Last höher als Push
|
||||
- → Migration zu Supabase Realtime ist Drop-In auf gleichem Datenmodell, deferrable bis "viele aktive" tatsächlich passiert
|
||||
|
||||
### Form-Builder UX
|
||||
|
||||
JSON-Editor in `/admin/feedback/[id]` mit Live-Preview-Pane:
|
||||
- + Schnell zu bauen, m kann formulieren wie er will
|
||||
- + Gut für Versionskontrolle / Wiederverwendung (m kann JSON teilen)
|
||||
- − Nicht-technische Owner ausgeschlossen (für v1 ist nur m owner — irrelevant)
|
||||
- → Drag-Drop-Builder als separates Issue wenn Trusted-Tier kommt und HL-Kollegen ihre eigenen Forms bauen sollen
|
||||
|
||||
### Mode: Form OR Chat OR Both
|
||||
|
||||
Schema trägt beides parallel (`form_definition` nullable, `chat_enabled` bool, CHECK-Constraint dass mindestens eines an ist):
|
||||
- + Maximum Flexibilität ohne Schema-Aufwand
|
||||
- + UI rendert was konfiguriert ist — keine Modus-Verzweigung
|
||||
- − Wenn m beides nicht will, brauchen wir trotzdem keine separate Schema-Variante
|
||||
|
||||
m's Frage 4 ("Kombi-Modus wirklich gewünscht?") → **Ja, Schema unterstützt es zero-cost**. Falls m das verbieten will, einfach im Admin-UI nicht beides gleichzeitig erlauben — schnelles Update, keine Migration.
|
||||
|
||||
### Persistenz für Teilnehmer
|
||||
|
||||
LocalStorage:
|
||||
- `feedback:display_name` (global, alle Instances)
|
||||
- `feedback:session:<slug>` = `{ session_id, submitted_at?, posts: [...] }`
|
||||
|
||||
Server kennt nur `client_session_id` — kein Cookie, kein Tracking-Cookie-Banner-Theater. Browser-isolated.
|
||||
|
||||
### IP-Speicherung
|
||||
|
||||
`client_ip` + `user_agent` werden gespeichert für Abuse-Forensik. **Empfehlung**: nach 30 Tagen automatisch nullen (separate Cron-Migration, später). v1 schreibt einfach.
|
||||
|
||||
### Eindeutiger Owner
|
||||
|
||||
`owner_user_id` → m. Trusted-Tier-Owner kommt mit Track D, nicht jetzt. Admin-Routes prüfen bewusst nicht "is_owner == auth.uid" sondern nur `requireAuth` — m ist alleiniger Auth-Nutzer in v1. Wenn Trusted-Tier kommt: Filter ergänzen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Open Questions — Vorgeschlagene Defaults für m's Gate
|
||||
|
||||
Jede Frage mit empfohlenem Default. **m: 👍 alle, oder einzelne überschreiben.**
|
||||
|
||||
| # | Frage | Mein Default | Rationale |
|
||||
|---|---|---|---|
|
||||
| 1 | Form-Builder-UX: Drag-Drop oder JSON? | **JSON-Editor mit Live-Preview** | m ist technisch, JSON ist schnell zu bauen, Builder kommt mit Trusted-Tier |
|
||||
| 2 | Live-Chat-Posts editierbar/löschbar? | **m moderiert via Hide-Toggle (soft-delete). Teilnehmer kann NICHT eigenes löschen.** | Anonyme Identität → kein verlässliches "ist meiner". Cleaner als Half-Implementation |
|
||||
| 3 | Reaktionen (👍 etc.) auf Posts? | **Nein in v1.** | Schema-Erweiterung trivial wenn gewünscht; nicht im Critical Path |
|
||||
| 4 | Kombi-Modus Form+Chat parallel? | **Ja, Schema trägt beides parallel.** UI rendert was konfiguriert ist. | Zero zusätzlicher Aufwand vs. mode-enum, max Flexibilität |
|
||||
| 5 | Persistenz Teilnehmer (Name + Chronologie)? | **Ja, LocalStorage.** Name global pre-filled, eigene Posts hervorgehoben. | UX-Selbstverständlichkeit, kein Cookie-Banner |
|
||||
| 6 | Closing-Mechanik? | **Ja, status='closed' sperrt POST (423), GET bleibt open für Read-Only.** | Standard-Pattern, m braucht Kill-Switch |
|
||||
|
||||
Zusätzliche Fragen, die ich auch noch m stellen würde:
|
||||
|
||||
| # | Frage | Mein Default | Rationale |
|
||||
|---|---|---|---|
|
||||
| 7 | Sichtbarkeit Live-Chat: alle sehen alles, oder Posts erst nach m's Approval? | **Sofort sichtbar** für alle Teilnehmer. m kann hidden setzen. | Teams-Stil, Q&A-Style; "Approval-First" ist eigener Modus → später |
|
||||
| 8 | Anonyme Posts erlauben (display_name ganz weg) oder zwingen "Anonym" als Default-Label? | **`display_name=NULL` → UI zeigt "anonym"** | Konsistent, kein Ghost-Cell |
|
||||
| 9 | Session-Expiry für `client_session_id`? | **Lebt für die LocalStorage-Lifetime** (kein Server-Expiry) | Einfach, anonyme Posts brauchen kein Expiry |
|
||||
| 10 | Notification an m wenn neue Submission/Post? | **Nein in v1.** Optional via `gotify` später nachrüstbar. | Out of scope, m schaut Admin-View während Event |
|
||||
|
||||
---
|
||||
|
||||
## 10. Implementation-Roadmap (für Coder-Shift, falls m grünlicht)
|
||||
|
||||
Tracer-Bullet-Order — jeder Schritt liefert eine durchdachte Inkrement-Demo:
|
||||
|
||||
1. **DB-Migration** + `feedback_instances` Helper im `flex()`-Stil
|
||||
2. **Public-Scope Allowlist**: `/^\/api\/public\/feedback(\/|$)/` ergänzen in `public-scope.ts`
|
||||
3. **Zod-Schemas** für Form-Definition + Submit + Post Body
|
||||
4. **Backend-API**: `GET /api/public/feedback/[slug]`, `POST /submit`, `GET /posts`, `POST /posts` — minimal, mit In-Memory Rate-Limit
|
||||
5. **Frontend `/f/[slug]`**: Form-Renderer + Chat-Liste + Polling — mobile-first
|
||||
6. **Admin-Liste + Create**: `/admin/feedback` mit JSON-Editor
|
||||
7. **Admin-Detail + Moderate**: `/admin/feedback/[id]` mit Tabs + Hide-Toggle
|
||||
8. **Export**: CSV + JSON Endpoints
|
||||
9. **Closing-Mechanik** + Edge-Cases (closed → 423)
|
||||
10. **Smoke-Test mit Playwright**: Form-Submit, Chat-Post, Hide, Close, Export
|
||||
11. **noindex robots.txt + meta**
|
||||
12. **Nav-Eintrag in `/admin/feedback`** unter "Tools" (auth-only)
|
||||
|
||||
Pro Step ein Commit, am Ende Merge-PR an main. Coder-Shift-Empfehlung: **mai-coder** (Standard-Worker, keine Spezial-Skills nötig — kein Realtime, kein WebSocket).
|
||||
|
||||
---
|
||||
|
||||
## 11. Eigene Eignung für die Implementation
|
||||
|
||||
**Knuth (ich)** habe in dieser Shift den Stack vermessen, Patterns gelesen, Schema designt. Ich kenne die Code-Konventionen jetzt out-of-the-box. Wenn m Track-Continuity will, bin ich gut aufgestellt für Coder-Shift-2.
|
||||
|
||||
Alternativ: jeder mai-coder mit Memory-Search und Lesen dieses Docs kommt in einer Stunde rein. Keine speziellen Skills nötig.
|
||||
|
||||
m / head entscheidet.
|
||||
|
||||
---
|
||||
|
||||
## 12. References
|
||||
|
||||
- Issue: https://mgit.msbls.de/m/flexsiebels.de/issues/63
|
||||
- Surface-Boundaries-PRD: m/otto `docs/plans/persona-surface-boundaries-prd.md` — Q4 narrow public passt
|
||||
- Public-Scope-Policy: `website/src/lib/server/public-scope.ts` (issue #59)
|
||||
- Schema-Helpers: `website/src/lib/server/flexsiebels.ts`, `mbrian.ts`
|
||||
- Vergleichs-Patterns: Microsoft Forms (Form-Mode), Microsoft Teams Live-Q&A (Chat-Mode), Slido (Live-Polls)
|
||||
Reference in New Issue
Block a user