mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
364 lines
20 KiB
Markdown
364 lines
20 KiB
Markdown
# Forms — Module Plan
|
||
|
||
## Status (2026-04-28)
|
||
|
||
**Noch nicht begonnen.** Plan-Dokument; Modul existiert nicht im Repo. Lücke explizit dokumentiert in [`docs/reports/clubdesk-vs-mana-comparison.md:106`](../reports/clubdesk-vs-mana-comparison.md): *"Quiz-Builder vorhanden; kein dediziertes Formular-/Anmelde-System."*
|
||
|
||
---
|
||
|
||
## Ziel
|
||
|
||
Ein Modul `forms`, in dem der Nutzer **eigene Formulare** baut und Antworten sammelt. Kernfrage: *"Wie kriege ich strukturierte Eingaben von anderen Menschen in mein Mana?"*
|
||
|
||
Use-Cases:
|
||
|
||
- Vereins-Anmeldung (löst die ClubDesk-Lücke)
|
||
- Event-Registrierung mit benutzerdefinierten Feldern (über `mana-events` Share-Links hinaus)
|
||
- Lead-Capture auf einer Website (Block in `website`)
|
||
- Wöchentliche Mood-/Pulse-Checks ans Team via `broadcasts`
|
||
- Persönliche Intake-Formulare (Onboarding-Fragebogen für Coaches/Therapeut:innen)
|
||
- "Wishlist"-Formular an Freunde (Geburtstags-Geschenkideen abfragen)
|
||
- RSVP-mit-Meta ("Kommst du? Was bringst du mit? Allergien?")
|
||
|
||
Nicht im Scope:
|
||
|
||
- **Quizze** — bleibt in `quiz` (Score-Semantik richtig/falsch, Play-Mode, kein Antwort-Sammeln)
|
||
- **Self-Surveys** an sich selbst — gehört in `journal` / `augur` / `habits`
|
||
- **Voting / Polls mit Live-Ergebnis-Anzeige** — eigenes Pattern, ggf. später als `polls`-Modul
|
||
- **Konversationelle Bots** — `mana-persona-runner` ist die Heimat dafür; Forms kann allerdings als *Form-as-Conversation* serviert werden (siehe Killer-Feature 4)
|
||
|
||
## Abgrenzung
|
||
|
||
- **Nicht `quiz`**: Score-frei. Ein Form-Eintrag hat keine "richtige" Antwort. Trotzdem teilen sich beide Module die Frage-Schema-Engine (siehe Architektur).
|
||
- **Nicht `feedback`**: `feedback` ist *zentral* der öffentliche Mana-Feedback-Hub (1 Hub für alle User). `forms` ist *n-zu-1 pro User/Space* — jeder User kann beliebig viele Formulare anlegen.
|
||
- **Nicht `website`**: `website` ist Block-Tree CMS für Public-Sites. `forms` ist die Form-Domain — wird in `website` als **Block** gerendert (analog wie `events` als Listing-Block).
|
||
- **Nicht `events`**: Events sind Termine. Forms sind Eingabe-Schemas. Ein Event *kann* ein Form als RSVP-Hook haben (1:1-Bindung), aber das Form-Modul kennt Events nicht direkt — das Event-Modul referenziert den Form.
|
||
- **Nicht `broadcasts`**: Broadcasts ist Distribution. Forms ist Capture. Ein Broadcast kann ein Form-Link enthalten; bei wiederkehrenden Forms (Killer-Feature 5) sendet `broadcasts` den Link periodisch.
|
||
|
||
## Architektur-Entscheidung
|
||
|
||
### Eigenes Modul, geteilte Schema-Engine mit `quiz`
|
||
|
||
- Eigenes Modul `forms` mit eigenem Launcher-Eintrag, eigener Route, eigener Encryption-Registry-Zeile.
|
||
- **Frage-Schema-Engine (Field-Types, Validation, Branching) wird in ein neues Shared-Package `@mana/shared-form-schema` extrahiert** — `quiz` und `forms` nutzen es. Mache das in M1 in zwei Schritten: zuerst `forms` baut die Engine inline; sobald sie stabil ist, extrahieren und `quiz` darauf migrieren (separate Lieferung, nicht in diesem Plan).
|
||
- **Eine Tabelle pro Belang**: `forms` (das Schema) und `formResponses` (die eingereichten Antworten). Kein gemeinsames JSON.
|
||
|
||
Begründung: Quiz und Forms haben *fundamental unterschiedliche* Lifecycle-Semantik (Quiz hat `quizAttempts` mit `correct`-Flag, Forms hat `formResponses` ohne Bewertung). Aber die Frage-Definition (Single-Choice, Multi-Choice, Long-Text, Required, etc.) ist identisch. Schema-Sharing reduziert Duplikate; Tabellen-Trennung hält Semantik sauber.
|
||
|
||
### Eine Public-Render-Pipeline für alle "Public Capture" Modi
|
||
|
||
Mana hat schon das **Visibility + Unlisted-Sharing System** (`@mana/shared-privacy`). Public-Form-Submission läuft darüber:
|
||
- `forms.visibility = 'unlisted'` mit Token → `/forms/<token>` öffentlich erreichbar
|
||
- Antwort-Submit geht über `mana-api` `/api/v1/forms/public/<token>/submit` (dem unlisted-System analog)
|
||
- Kein neuer Auth-Flow; Public-Submitter müssen *nicht* eingeloggt sein
|
||
|
||
## Modul-Struktur
|
||
|
||
```
|
||
apps/mana/apps/web/src/lib/modules/forms/
|
||
├── types.ts # LocalForm, Form, FormField, FormResponse, FieldType, BranchingRule
|
||
├── collections.ts # forms + formResponses Dexie-Tables + Welcome-Seed (1 Beispiel-Form pro Space)
|
||
├── queries.ts # useAllForms, useForm(id), useResponses(formId), useResponseStats(formId)
|
||
├── stores/
|
||
│ ├── forms.svelte.ts # createForm, updateForm, addField, reorderFields, setVisibility, regenerateToken
|
||
│ └── responses.svelte.ts # submitResponse, deleteResponse, exportCsv
|
||
├── components/
|
||
│ ├── FieldEditor.svelte # Drag-reorder, type-switch, required toggle, options editor
|
||
│ ├── FieldRenderer.svelte # Public-side: rendert ein Feld als Input
|
||
│ ├── FieldPalette.svelte # "Feld hinzufügen"-Picker
|
||
│ ├── BranchingEditor.svelte # "Wenn Feld X = Y, zeige Feld Z"
|
||
│ ├── ResponsePreview.svelte # Tabellen-Zeile pro Antwort, Klick → Detail
|
||
│ ├── ResponseDetailModal.svelte
|
||
│ └── FormBlock.svelte # Embed-Komponente für website-builder
|
||
├── views/
|
||
│ ├── ListView.svelte # Form-Übersicht (Karten mit Antwort-Count)
|
||
│ ├── BuilderView.svelte # Form-Editor (Felder + Settings + Logik)
|
||
│ ├── ResponsesView.svelte # Antwort-Tabelle mit CSV-Export
|
||
│ ├── PublicFormView.svelte # Public-Submit-UI (rendert die definierten Felder)
|
||
│ └── SettingsView.svelte # Form-Level Settings (Anonymität, ZK, Auto-Sync-Targets)
|
||
├── tools.ts # AI-Tools (siehe AI-Integration)
|
||
├── lib/
|
||
│ ├── validation.ts # Field-Type-Validation pro Type
|
||
│ ├── branching.ts # Conditional-Logic-Resolver
|
||
│ └── csv.ts # CSV-Export pure
|
||
├── module.config.ts # { appId: 'forms', tables: [{ name: 'forms' }, { name: 'formResponses' }] }
|
||
└── index.ts
|
||
```
|
||
|
||
## Daten-Schema
|
||
|
||
### `LocalForm` (Dexie)
|
||
|
||
```typescript
|
||
export type FieldType =
|
||
| 'short_text'
|
||
| 'long_text'
|
||
| 'single_choice'
|
||
| 'multi_choice'
|
||
| 'number'
|
||
| 'date'
|
||
| 'email'
|
||
| 'yes_no'
|
||
| 'rating' // 1-5 oder 1-10
|
||
| 'section' // visueller Trenner, kein Input
|
||
| 'consent'; // Pflicht-Häkchen mit Text (DSGVO)
|
||
|
||
export interface FormField {
|
||
id: string; // stabile UUID, von der UI generiert
|
||
type: FieldType;
|
||
label: string;
|
||
helpText?: string;
|
||
required: boolean;
|
||
options?: { id: string; label: string }[]; // single/multi
|
||
config?: {
|
||
minLength?: number;
|
||
maxLength?: number;
|
||
min?: number;
|
||
max?: number;
|
||
ratingScale?: 5 | 10;
|
||
};
|
||
}
|
||
|
||
export interface BranchingRule {
|
||
id: string;
|
||
ifFieldId: string;
|
||
ifOperator: 'equals' | 'not_equals' | 'contains' | 'is_empty';
|
||
ifValue?: string | string[];
|
||
thenAction: 'show' | 'hide' | 'skip_to';
|
||
thenFieldIds?: string[];
|
||
thenSkipToFieldId?: string;
|
||
}
|
||
|
||
export interface LocalForm extends BaseRecord {
|
||
title: string;
|
||
description: string | null;
|
||
fields: FormField[]; // ≤ 100 (soft-cap)
|
||
branching: BranchingRule[];
|
||
status: 'draft' | 'published' | 'closed';
|
||
settings: {
|
||
submitButtonLabel: string;
|
||
successMessage: string;
|
||
allowMultipleSubmissions: boolean;
|
||
requireEmail: boolean;
|
||
anonymous: boolean; // wenn true: keine Submitter-Meta gespeichert
|
||
zkMode: boolean; // wenn true: per-Form-Key, Owner sieht nur Aggregate
|
||
closedAt?: string;
|
||
responseLimit?: number;
|
||
autoSync?: {
|
||
target: 'contacts' | 'events' | 'feedback' | 'library' | 'space_member';
|
||
mapping: Record<string, string>; // formFieldId → targetField
|
||
};
|
||
};
|
||
responseCount: number; // Denormalized counter
|
||
visibility?: VisibilityLevel;
|
||
visibilityChangedAt?: string;
|
||
visibilityChangedBy?: string;
|
||
unlistedToken?: string;
|
||
unlistedExpiresAt?: string;
|
||
}
|
||
|
||
export interface LocalFormResponse extends BaseRecord {
|
||
formId: string;
|
||
submittedAt: string;
|
||
answers: Record<string, string | string[] | number | boolean | null>; // fieldId → value
|
||
submitterEmail?: string; // optional, encrypted
|
||
submitterName?: string; // optional, encrypted
|
||
submitterMeta?: { // encrypted blob
|
||
userAgent?: string;
|
||
referrer?: string;
|
||
ipHash?: string; // server-side gehasht, nie raw IP
|
||
};
|
||
status: 'new' | 'reviewed' | 'archived' | 'spam';
|
||
syncedTargets?: { target: string; recordId: string }[]; // wenn autoSync angewandt
|
||
}
|
||
```
|
||
|
||
### Encryption-Registry (`crypto/registry.ts`)
|
||
|
||
```typescript
|
||
forms: {
|
||
encrypt: ['title', 'description', 'fields', 'branching', 'settings'],
|
||
plaintext: ['status', 'responseCount', 'visibility', 'visibilityChangedAt',
|
||
'visibilityChangedBy', 'unlistedToken', 'unlistedExpiresAt',
|
||
'createdAt', 'updatedAt', 'spaceId', 'id'],
|
||
},
|
||
formResponses: {
|
||
encrypt: ['answers', 'submitterEmail', 'submitterName', 'submitterMeta'],
|
||
plaintext: ['formId', 'submittedAt', 'status', 'syncedTargets',
|
||
'createdAt', 'updatedAt', 'spaceId', 'id'],
|
||
},
|
||
```
|
||
|
||
`submittedAt` bleibt plaintext (Sortier-Key); `syncedTargets` plaintext, weil es Cross-Module-IDs ohne PII enthält.
|
||
|
||
## AI-Integration
|
||
|
||
### Tools im `AI_TOOL_CATALOG`
|
||
|
||
| Tool | Mode | Zweck |
|
||
|---|---|---|
|
||
| `forms_create` | propose | Neues Form aus Prompt — Felder werden generiert, User reviewed |
|
||
| `forms_add_field` | propose | Einzelnes Feld einfügen (z.B. "füge ein Allergien-Feld hinzu") |
|
||
| `forms_publish` | propose | draft → published, generiert unlisted-Token |
|
||
| `forms_close` | propose | published → closed (keine neuen Antworten) |
|
||
| `forms_list` | auto | Forms eines Spaces auflisten (nur Metadaten) |
|
||
| `forms_get_responses` | auto | Antworten als Aggregat (counts, top-values pro Feld) |
|
||
| `forms_summarize_responses` | auto | LLM clustert offene Text-Antworten + zieht Themes (analog Augur Living-Oracle) |
|
||
|
||
### Mission-Runner-Integration
|
||
|
||
Forms ist ein **prime candidate für Mission-Runner**: "Bau mir bis Donnerstag ein Form für Vereins-Anmeldung mit Pflichtfeldern X, Y, Z, schicke den Link an die Adressliste-A, und melde mir am Sonntag die Antworten." → Mission spannt forms_create + broadcasts_send + (in 5 Tagen) forms_summarize_responses.
|
||
|
||
## Sync
|
||
|
||
- `forms` und `formResponses` syncen über die unified `mana-sync`-Engine wie alle anderen Tabellen — pro `appId='forms'` gegruppiert.
|
||
- **Public-Submit-Pfad ist eigene Lane**: `mana-api` exponiert `POST /api/v1/forms/public/<token>/submit`, schreibt in `mana_platform.forms_responses` Schema, schickt das Insert *server-seitig* in die Sync-Pipeline (analog `unlisted-snapshots`-Resolver). Der einreichende Nutzer hat keinen Sync-Client.
|
||
- Edge-Case **Anonymous-Mode**: bei `settings.anonymous=true` werden `submitterEmail`/`submitterName`/`submitterMeta` server-seitig vor dem Insert verworfen. Dem Owner-Client wird nichts angeliefert, was er nicht selber erst entschlüsseln dürfte.
|
||
|
||
## Visibility & Unlisted Sharing
|
||
|
||
Ein Form hat zwei Bedeutungen von "öffentlich":
|
||
|
||
1. **Form-Schema öffentlich** — `visibility='unlisted'` → Token-Link zum *Submit*-View
|
||
2. **Antworten öffentlich** — separates Flag `settings.responsesPublic` (rare; standardmäßig **immer privat**)
|
||
|
||
Default: ein neues Form ist `visibility='private'`. "Form veröffentlichen" bedeutet `published` Status + `unlisted` Visibility-Bump — beides in *einem* Klick im Builder.
|
||
|
||
Token-Rotation/-Expiry analog zum bestehenden Unlisted-System (`@mana/shared-privacy/unlisted-client`).
|
||
|
||
## Killer-Features (Differentiator vs. Typeform/Tally)
|
||
|
||
Diese drei Features rechtfertigen, dass wir Forms statt eines Typeform-Embeds bauen. Alles andere ist Commodity.
|
||
|
||
### KF1 — AI-Form-from-Prompt
|
||
"Bau mir ein Anmeldeformular für unseren Volleyball-Verein mit Spielposition, Trikotgröße und DSGVO-Häkchen." → `forms_create` füllt alle Felder vor; User reviewed im Builder; ein Klick zum Publish.
|
||
|
||
### KF4 — Form-as-Conversation via Persona-Runner
|
||
Der Public-Form-View hat einen Toggle `experience: 'classic' | 'conversation'`. Im Conversation-Mode rendert sich das Form als Chat — `mana-persona-runner` mit einer "Form-Host"-Persona stellt die Fragen, akzeptiert Free-Text-Antworten, mapt sie zurück auf strukturierte Felder. Erfordert `mana-persona-runner` Public-Mode (existiert noch nicht — siehe M5).
|
||
|
||
### KF7 — Public Sign-up an Space
|
||
Form mit `settings.autoSync.target='space_member'` → Submit erzeugt einen Space-Membership-Invite. Löst die ClubDesk-"Online-Anmeldung"-Lücke direkt; der Submitter klickt im Bestätigungs-Email auf "Beitreten" und wird Mitglied.
|
||
|
||
---
|
||
|
||
## Milestones
|
||
|
||
### M1 — Skelett + Field-Engine inline
|
||
- Modul registriert in `mana-apps.ts` (icon, color, tier, requiredTier='guest')
|
||
- Dexie v53 (oder nächste freie Version) mit `forms` + `formResponses` Tables + Per-Space-Welcome-Seed (ein Beispiel-Form: "Mini-Feedback")
|
||
- Encryption-Registry erweitert
|
||
- Route `/forms` mountet mit Empty-State
|
||
- Field-Engine inline (Validation, Branching) — noch kein Shared-Package-Extract
|
||
|
||
**Done:** `/forms` lädt; `pnpm validate:all` grün.
|
||
|
||
### M2 — Builder + CRUD
|
||
- `BuilderView`: Drag-reorder Felder via existierende Drag-Primitives, FieldPalette mit den 11 Feldtypen, FieldEditor pro Feld inline
|
||
- Field-Type-spezifische Konfig (Options-Editor für Choice-Felder, Min/Max für Number, Rating-Scale-Toggle)
|
||
- Form-Settings-Panel (submitButtonLabel, successMessage, requireEmail, allowMultipleSubmissions)
|
||
- Draft-Save: every change → `forms.update` (autosave-on-blur Pattern wie in `lasts`)
|
||
- ListView mit Karten pro Form, Klick → BuilderView
|
||
|
||
**Done:** Form mit ≥10 Feldern lässt sich bauen, speichern, neu laden.
|
||
|
||
### M3 — Public-Submit + Responses-Inbox
|
||
- `mana-api` `POST /api/v1/forms/public/<token>/submit` (Hono-Route in `services/mana-api` oder bestehender unified `apps/api`)
|
||
- `PublicFormView` rendert die definierten Felder, validiert client-side, submitet
|
||
- Bei Submit: Response-Insert in mana_platform → mana-sync pickt's auf → kommt im Owner-Client als neue Zeile in `formResponses` an
|
||
- `ResponsesView`: Tabelle mit Submit-Time, Submitter-Email, Snippet pro Antwort; Klick → ResponseDetailModal mit allen Antworten
|
||
- CSV-Export pure (kein Server-Roundtrip)
|
||
- Status-Workflow `new → reviewed → archived` per Klick in der Tabelle
|
||
|
||
**Done:** Form publishen, Link in Inkognito-Tab öffnen, Antwort einreichen, Owner sieht sie ≤2 Sek nach Sync.
|
||
|
||
### M4 — Conditional Logic + Visibility
|
||
- `BranchingEditor` mit "Wenn-Dann"-Regeln (UI: pro Feld ein "Logik"-Tab)
|
||
- `branching.ts` Resolver berechnet effektiv-sichtbare Felder pro Antwortzustand
|
||
- Visibility-System angeschlossen: `<VisibilityPicker>` + `<SharedLinkControls>` im Builder
|
||
- Token-Rotation + Expiry
|
||
- Resolver `buildFormPublicBlob` in `data/unlisted/resolvers.ts` (whitelist: title, description, fields, branching, settings.submitButtonLabel, settings.successMessage)
|
||
- Hard-Block: forms mit `responsesPublic: true` werden trotzdem nie ohne Login serialisiert (Antworten bleiben privat)
|
||
|
||
**Done:** Branching macht Felder sichtbar/unsichtbar je nach Antwort; Token-Link funktioniert; Token rotieren funktioniert.
|
||
|
||
### M5 — AI-Tools
|
||
- 7 Tools (siehe AI-Integration) in `@mana/shared-ai/src/tools/schemas.ts`
|
||
- Webapp-Implementierungen in `forms/tools.ts`
|
||
- Server-seitige planner-drift-tests grün
|
||
- `forms_summarize_responses` ruft `mana-llm` mit den (entschlüsselten, client-seitig) Antworten auf — keine Klartext-PII ans LLM, ggf. mit Augur-Style "redact emails/names"-Pre-Pass
|
||
|
||
**Done:** "Bau mir ein Form für …" über Workbench-Chat → fertiges Form im Builder.
|
||
|
||
### M6 — Encryption + ZK-Mode
|
||
- ZK-Mode-Toggle: bei aktiv wird ein per-Form-Key generiert (separater Eintrag in mana-auth Master-Key-Store, *nicht* der User-Master-Key)
|
||
- Public-Form-Antworten werden client-seitig im Submitter-Browser mit dem Form-Key (aus dem Public-Schema-Bundle) verschlüsselt → Server speichert nur Ciphertext
|
||
- Owner-Client hat den Key (im eigenen, master-key-encrypted Storage), kann lokal entschlüsseln
|
||
- Aggregat-Tools (`forms_get_responses` count-mode) funktionieren *ohne* Decrypt — Server kann zählen ohne lesen
|
||
- Encrypted-tools-audit grün; Crypto-Registry-Update für `formResponses.answers`
|
||
|
||
**Done:** ZK-Form publishen, Submit, Server-DB direkt inspizieren → kein Klartext-Antwort sichtbar.
|
||
|
||
### M7 — Auto-Sync zu Cross-Modulen
|
||
- `settings.autoSync` UI im Settings-Tab des Builders
|
||
- Bei Submit: Response-Insert + Mapping → schreibt in Target-Modul-Tabelle
|
||
- 5 Targets: contacts, events (RSVP), feedback, library, space_member
|
||
- `syncedTargets` auf der Response zeigt was passiert ist (UI: kleine Chips in der Response-Detail)
|
||
|
||
**Done:** Form mit autoSync auf `contacts` ausfüllen → neuer Kontakt erscheint im `/contacts`-Modul.
|
||
|
||
### M8 — Website-Block + Embed
|
||
- `FormBlock.svelte` als Block in `website` Block-Tree CMS
|
||
- Block-Config: form_id-Picker (nur eigene Forms des aktiven Spaces)
|
||
- SSR-Render auf der public Website rendert das Form direkt (kein Iframe)
|
||
- Submit landet im selben Public-API-Endpoint
|
||
|
||
**Done:** Form als Block in eine Website-Page einfügen, Page öffnen, Form ausfüllen, Antwort kommt im Owner-Client an.
|
||
|
||
### M9 — Form-as-Conversation (KF4)
|
||
- Toggle `experience: 'classic' | 'conversation'` in Form-Settings
|
||
- "Form-Host"-Persona in `mana-persona-runner` (read-only, stateless, kein Tool-Access)
|
||
- Public-Conversation-View streamt Fragen über `mana-persona-runner` Public-API (neuer Endpoint)
|
||
- Antwort-Mapping: LLM extrahiert pro Feldtyp (Single-Choice → match closest option, Number → parse, etc.)
|
||
- Fallback bei Mapping-Fehler: classic-Mode-View mit pre-filled Werten
|
||
|
||
**Done:** Form im Conversation-Mode öffnen → Chat-Bubbles statt Inputs; nach Submit liegt strukturierte Response wie üblich vor.
|
||
|
||
### M10 — Wiederkehrende Forms (KF5)
|
||
- Form-Setting `recurrence: { frequency: 'weekly' | 'monthly', sendVia: 'broadcast', recipients: ContactList }`
|
||
- Cron-Job in `mana-notify` oder `mana-ai`: schickt Link periodisch
|
||
- Each-Send markiert Antworten mit `cohort: 'YYYY-WW'` für Trend-Analyse
|
||
- Cross-Module: bei `autoSync` zu `mood` → speist Augur-Living-Oracle
|
||
|
||
**Done:** Wöchentlicher Pulse-Check setup, läuft 4 Wochen, ResponseView zeigt Trend.
|
||
|
||
---
|
||
|
||
## Out-of-Scope (möglich später)
|
||
|
||
- **File-Upload-Field** — erfordert Storage-Quota-Management pro Form, eigenes Sub-Plan-Doc
|
||
- **Payment-Field** — Stripe/SEPA-Integration; eher Teil von `invoices`-Modul
|
||
- **A/B-Testing der Form-Varianten** — Cohort-Mechanik aus M10 reicht aus, A/B kann später drauf
|
||
- **Public-Results-Dashboard** — wenn `responsesPublic`, ein Aggregate-View ohne Login
|
||
- **Multi-Page Forms** mit Pagination — derzeit single-page, scrollbar; Branching reicht meist
|
||
- **Templates-Library** — wenn KF1 (AI-from-Prompt) zieht, brauchen wir keine Template-Galerie
|
||
|
||
## Nicht-Ziele
|
||
|
||
- **Logic-Engine wird nicht turing-complete** — nur if/then/skip; keine Formel-Felder ("Antwort A * 2 + Antwort B")
|
||
- **Keine Webhooks** zu Drittsystemen — wer das will, scriptet es via `mana-mcp` Tools
|
||
- **Kein eigener Server-Dienst** — `apps/api` reicht; kein neuer `services/mana-forms`-Container
|
||
|
||
## Validatoren-Checkliste vor jedem Push
|
||
|
||
- `pnpm validate:all` grün (turbo + pgSchema + theme + i18n + crypto)
|
||
- `pnpm test --filter @mana/web -- forms` grün
|
||
- `audit:encrypted-tools` zählt `forms` mit
|
||
- `validate:i18n-parity` mit Namespace `forms/` × 5 Locales aligned
|
||
|
||
## Vorbild-Module für Pattern-Recyceln
|
||
|
||
- **`quiz`** — Frage-Schema, Field-Types-Pattern (extrahieren in M1-Folge-Lieferung)
|
||
- **`lasts`** — Settings-Store, DueBanner für "Du hast 3 neue Antworten", autosave-on-blur
|
||
- **`augur`** — Living-Oracle für `forms_summarize_responses` (Theme-Clustering)
|
||
- **`events`** Share-Links — Public-Submit-Pfad-Vorbild für Anmeldung
|
||
- **`website`** Block-Tree — `FormBlock.svelte` analog zu existierenden Blocks
|