From ba6274edbefd724482129c14bfa551f716245656 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 26 Apr 2026 21:52:25 +0200 Subject: [PATCH] refactor(feedback): align package + DB enums, plan central hub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Macht @mana/feedback zur SSOT für alle Nutzer-Feedback-Categories und -Status — Voraussetzung dafür, dass Onboarding-Wishes, NPS, Churn-Feedback etc. künftig dort landen. - Status-Enum: DB-Werte umbenannt new/reviewed/done/rejected → submitted/under_review/completed/declined (Package gewinnt). PG≥10 ALTER TYPE … RENAME VALUE ist non-destructive. - Category 'praise' ins Package aufgenommen (war nur in DB). - Category 'onboarding-wish' neu in Package + DB für den Wish-Step. - Default status in DB: 'new' → 'submitted'. - CreateFeedbackInput.isPublic optional → Service reicht durch, default bleibt true; private Categories wie onboarding-wish setzen false. - Schema-Datei mit SSOT-Kommentar versehen, der Drift in Zukunft verhindert. Hand-authored Migration unter services/mana-analytics/drizzle/0001_*.sql weil drizzle-kit push Enum-Werte nicht zuverlässig umbenennt. Manuell einspielen vor nächstem db:push: psql "\$DATABASE_URL" -f services/mana-analytics/drizzle/0001_align-feedback-enums.sql Plan in docs/plans/feedback-hub.md (Phase 0–4); Phase 0 + 1 jetzt, 2-4 deferred. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/plans/feedback-hub.md | 236 ++++++++++++++++++ packages/feedback/src/api.ts | 6 + packages/feedback/src/feedback.ts | 18 +- .../drizzle/0001_align-feedback-enums.sql | 66 +++++ .../mana-analytics/src/db/schema/feedback.ts | 15 +- .../mana-analytics/src/services/feedback.ts | 5 + 6 files changed, 339 insertions(+), 7 deletions(-) create mode 100644 docs/plans/feedback-hub.md create mode 100644 services/mana-analytics/drizzle/0001_align-feedback-enums.sql diff --git a/docs/plans/feedback-hub.md b/docs/plans/feedback-hub.md new file mode 100644 index 000000000..76e927eed --- /dev/null +++ b/docs/plans/feedback-hub.md @@ -0,0 +1,236 @@ +--- +status: draft +owner: till +created: 2026-04-26 +--- + +# `@mana/feedback` als zentraler Feedback-Hub + +> Alle nutzergenerierten Rückmeldungen — Bug-Reports, Feature-Wünsche, Lob, +> Onboarding-Wünsche, NPS, Churn-Gründe — landen in einem System: dem +> `user_feedback`-Table im `mana-analytics`-Service, getypt über das +> `@mana/feedback`-Package. Ein Schema, ein API-Surface, ein Admin-Hub. + +--- + +## Ist-Zustand (2026-04-26) + +- **Package** `packages/feedback/` exportiert Typen, einen Service-Factory + (`createFeedbackService`), und UI-Komponenten (`FeedbackPage`, + `FeedbackForm`, `FeedbackList`, `VoteButton`, `StatusBadge`). +- **Server** `services/mana-analytics` (Port 3064): Postgres-Schema + `feedback`, Tabellen `user_feedback` + `feedback_votes`, REST-Endpoints + unter `/api/v1/feedback/*`. Auto-Title via `mana-llm` beim Submit. +- **Web-App** Singleton in `apps/mana/apps/web/src/lib/api/feedback.ts`, + Modul-View in `apps/mana/apps/web/src/lib/modules/feedback/ListView.svelte`, + Route `/feedback`. Voting + Public-Liste sind voll verdrahtet. +- **Drift**, die fixen müssen, bevor wir mehr draufpacken: + | Bereich | Package | DB | + |----------|--------------------------------------------|--------------------------------------| + | Status | `submitted/under_review/.../completed/declined` | `new/reviewed/.../done/rejected` | + | Category | `bug/feature/improvement/question/other` | + `praise` (zusätzlich) | + | Default | `submitted` (impliziert) | `new` | + Konsequenz: Client typisiert Status als `'submitted'`, kriegt aber + `'new'` zurück → `FEEDBACK_STATUS_CONFIG[status]` ist `undefined` → + StatusBadge rendert leise nichts. Niemand fällt's auf weil keine + Admin-UI Status setzt und alle Records auf Default sitzen. +- **Fundamentale Annahmen, die wir nicht ändern:** + - Server-only Persistence (kein Dexie / Local-First, kein mana-sync). + Submit ist ein einziger POST, fail-soft. + - Feedback ist **nicht** im Mana-Crypto-Pfad — Klartext im DB. + OK für Bug-Reports & Wünsche; sensible Daten gehören eh nicht hier rein. + +--- + +## Phase 0 — Drift fixen *(Refactor, ein Commit)* + +Ziel: Package + DB konsistent, Defaults sauber, keine Funktions-Erweiterung. + +### 0a. Status-Enum: Package gewinnt + +PostgreSQL kann seit 10 `ALTER TYPE ... RENAME VALUE`, das ist non-destructive +und behält die Sortierung der Enum-Werte. Wir benennen die DB-Werte um, sodass +sie zum Package passen: + +```sql +ALTER TYPE feedback.feedback_status RENAME VALUE 'new' TO 'submitted'; +ALTER TYPE feedback.feedback_status RENAME VALUE 'reviewed' TO 'under_review'; +ALTER TYPE feedback.feedback_status RENAME VALUE 'done' TO 'completed'; +ALTER TYPE feedback.feedback_status RENAME VALUE 'rejected' TO 'declined'; +ALTER TABLE feedback.user_feedback ALTER COLUMN status SET DEFAULT 'submitted'; +``` + +Drizzle-Schema (`services/mana-analytics/src/db/schema/feedback.ts`) parallel +auf die neuen Werte ziehen, sodass `db:push` nicht versucht, neu anzulegen. + +### 0b. Category 'praise' ins Package aufnehmen + +Package hat `bug/feature/improvement/question/other`. DB hat `praise` +zusätzlich. Wir nehmen `'praise'` ins Package mit Label "Lob" und +behalten DB unverändert. + +### 0c. Single Source of Truth + +Mana-analytics importiert die Enum-Werte ab jetzt aus `@mana/feedback` +statt eigenes `pgEnum`-Array zu pflegen. Verhindert künftige Drift +strukturell. (Falls drizzle-kit das nicht direkt kann, dann mindestens +ein Test in `services/mana-analytics` der die Listen vergleicht.) + +### Migrations-Workflow + +mana-analytics benutzt aktuell `drizzle-kit push` (kein +Migrations-Verzeichnis). Für `ALTER TYPE RENAME VALUE` ist push nicht +zuverlässig — das ist ein hand-authored SQL-Step. + +→ Neue Datei `services/mana-analytics/drizzle/0001_align-feedback-enums.sql` +einführen, in Setup-README dokumentieren ("apply manually before db:push"). +Pattern wie `apps/api/drizzle/{schema}/*.sql`. + +--- + +## Phase 1 — Onboarding-Wish *(Feature, ein Commit)* + +Ziel: Letzter Onboarding-Schritt ist Freitext-Frage "Was wünschst du dir +von Mana?", deren Antwort als `@mana/feedback`-Record landet. + +### 1a. Neue Category `'onboarding-wish'` + +```sql +ALTER TYPE feedback.feedback_category ADD VALUE IF NOT EXISTS 'onboarding-wish'; +``` + +Im Package: +- `FeedbackCategory` um `'onboarding-wish'` erweitern +- `FEEDBACK_CATEGORY_LABELS['onboarding-wish'] = 'Was ich mir wünsche'` + +### 1b. Onboarding-Flow-Store erweitern + +`apps/mana/apps/web/src/lib/stores/onboarding-flow.svelte.ts`: +- `pendingWish: string | null` +- `setPendingWish(value)` / `reset()` mit dabei + +### 1c. Layout: 3 → 4 Step-Dots + +`apps/mana/apps/web/src/routes/(app)/onboarding/+layout.svelte`: +- `currentStep`-Mapping: `/onboarding/wish` → 3 +- Dots-Array `[0,1,2,3]` +- aria-valuemax = 4 + +### 1d. Neuer Screen `/onboarding/wish/+page.svelte` + +- **Aktivierungstext:** + > # Eine letzte Sache + > Was wünschst du dir von Mana? Wofür willst du's nutzen, was erhoffst du dir? + > + > Schreib einfach, wie's dir kommt — wir lesen jede Antwort und sie + > hilft uns, Mana für dich besser zu machen. +- **Textarea**: `maxlength=2000`, autofocus, `auto-grow` +- **Buttons**: Zurück (→ `/onboarding/templates`) + Fertig +- **Submit-Logik** (Fertig): + 1. Wenn Textarea nicht leer → `feedbackService.createFeedback({ + category: 'onboarding-wish', isPublic: false, feedbackText: trimmed })` + **fail-soft** (`try/catch`, nur `console.warn`, kein UI-Block) + 2. `onboardingStatus.markComplete()` + 3. `onboardingFlow.reset()` + 4. `goto('/')` +- **Wenn Textarea leer + Fertig**: gleicher Flow ohne Submit. +- **isPublic = false** by default, weil Wünsche persönliche Statements sind, + kein Public-Voting-Material. (Lässt sich später per Admin-Action publishen.) + +### 1e. Templates-Screen umbiegen + +`apps/mana/apps/web/src/routes/(app)/onboarding/templates/+page.svelte`: +- Fertig-Button heißt jetzt "Weiter" und routet `goto('/onboarding/wish')` +- `markComplete` + `reset` wandern raus aus templates → in den wish-Screen +- Templates-Save (createScene) bleibt wie er ist + +### Akzeptanzkriterien + +- 4 Step-Dots im Footer; bei `/onboarding/wish` ist Dot 4 aktiv +- Globaler Skip-Button (unten links) funktioniert auf allen 4 Screens + (markComplete + `/`) +- Submit von "Was wünschst du dir" landet als Row in `user_feedback` + mit `category='onboarding-wish'`, `is_public=false` +- Wenn `mana-analytics` nicht erreichbar ist, blockiert das Onboarding + nicht — User kommt trotzdem auf `/` +- Bestehende Public-Feedback-Liste auf `/feedback` zeigt + `onboarding-wish`-Records **nicht** (weil `is_public=false`) + +--- + +## Phase 2 — Hub-Charakter ausbauen *(separater Sprint)* + +Drei kleine Erweiterungen, die `@mana/feedback` zur "echten" Zentrale +machen. Können einzeln geshippt werden. + +### 2a. Globaler Feedback-Button + +Eintrag im Account-Menü oder PillNav ("Feedback / Idee teilen") öffnet +einen Modal mit der bestehenden `FeedbackForm`. Eliminiert das Risiko, +dass jemand pro Modul eigene Feedback-Buttons baut. + +### 2b. Inline-Hook pro Modul + +Komponente `` für Module-Help-Panels, +vorausgefüllt mit `appId`, Default-Category `'feature' | 'improvement'`. +Kontextspezifische Wünsche. + +### 2c. Admin-Triage-Hub `/feedback/admin` + +Founder-Tier-gated. Features: +- Filter: Kategorie, Status, Datum, App +- Bulk-Status-Updates +- `adminResponse` schreiben +- Aggregations-Card für `onboarding-wish`: alle Antworten gelistet, + optional via LLM nach Themen geclustert (was wollen neue Nutzer + am häufigsten?) +- Neue Backend-Endpoints: + - `PATCH /api/v1/feedback/admin/:id` (status, adminResponse, isPublic) + - `GET /api/v1/feedback/admin?category=...&status=...` (alle inkl. private) + +--- + +## Phase 3 — Future Categories *(Backlog, Schema-Slot offenhalten)* + +Damit das Schema nicht noch mal bricht, halten wir Platz für: + +- `'nps'` — Score 0-10 + optional Kommentar; nach 30 Tagen aktiver Nutzung + einmalig getriggert. Brauche dafür eine optionale `score INT`-Spalte + auf `user_feedback`. +- `'churn-feedback'` — wenn jemand den Account löscht: warum? + (Pflicht-Modal vor Final-Delete.) +- `'support-request'` — 1:1-Hilfe statt öffentlicher Bug. +- `'praise'` — schon in Phase 0 mitgenommen. + +→ NICHT jetzt bauen, nur als Roadmap-Marker. + +--- + +## Phase 4 — Local-First *(deferred)* + +Aktuell ist `@mana/feedback` Server-Direct-POST. Local-First lohnt sich erst, +wenn: +- Leute Feedback offline schreiben sollen (Mobile-Use-Case) +- `feedbackText` verschlüsselt im Sync laufen soll (privacy-relevant?) + +Bis dahin: Status quo. Dexie-Tabelle `feedbackEntries` + `crypto/registry.ts` ++ mana-sync field-level LWW wäre der Migrations-Plan. + +--- + +## Bekannte Drift, die wir hier NICHT angehen + +- **`apps/mana/apps/web/src/lib/api/feedback.ts`** schickt an + `getManaAuthUrl()/api/v1/feedback`. Aber `mana-analytics` (3064) ist die + echte Heimat. Funktioniert nur, wenn mana-auth proxiet. Eigener Fix-PR. +- **Feedback fehlt in `packages/shared-branding/src/mana-apps.ts`** — + ist nur in `apps/web/src/lib/app-registry/apps.ts`. Konsistent oder + bewusst? Nicht in dieser Plan-Iteration. + +--- + +## Reihenfolge & Commits + +1. **Commit 1 (Phase 0)**: `refactor(feedback): align package + DB enums, add 'praise' category` +2. **Commit 2 (Phase 1)**: `feat(onboarding): add wish step, route to feedback service` +3. **Phase 2/3/4**: separate Sprints, separate Plan-Updates. diff --git a/packages/feedback/src/api.ts b/packages/feedback/src/api.ts index 53e961dd1..7467d9026 100644 --- a/packages/feedback/src/api.ts +++ b/packages/feedback/src/api.ts @@ -8,6 +8,12 @@ export interface CreateFeedbackInput { title?: string; feedbackText: string; category?: FeedbackCategory; + /** + * Whether the submission shows up in the public community list. + * Defaults to `true` server-side. Set `false` for private intake + * categories like `onboarding-wish` or `churn-feedback`. + */ + isPublic?: boolean; deviceInfo?: Record; } diff --git a/packages/feedback/src/feedback.ts b/packages/feedback/src/feedback.ts index 731678592..29a9f861f 100644 --- a/packages/feedback/src/feedback.ts +++ b/packages/feedback/src/feedback.ts @@ -1,8 +1,20 @@ /** - * Core feedback types + * Core feedback types — Single source of truth for the @mana/feedback hub. + * + * Mana-analytics' Postgres enums (`feedback.feedback_category`, + * `feedback.feedback_status`) MUST mirror these literal unions exactly. + * If you add or rename a value here, also write a SQL migration under + * services/mana-analytics/drizzle/. */ -export type FeedbackCategory = 'bug' | 'feature' | 'improvement' | 'question' | 'other'; +export type FeedbackCategory = + | 'bug' + | 'feature' + | 'improvement' + | 'question' + | 'praise' + | 'onboarding-wish' + | 'other'; export type FeedbackStatus = | 'submitted' @@ -43,6 +55,8 @@ export const FEEDBACK_CATEGORY_LABELS: Record = { feature: 'Feature', improvement: 'Verbesserung', question: 'Frage', + praise: 'Lob', + 'onboarding-wish': 'Was ich mir wünsche', other: 'Sonstiges', }; diff --git a/services/mana-analytics/drizzle/0001_align-feedback-enums.sql b/services/mana-analytics/drizzle/0001_align-feedback-enums.sql new file mode 100644 index 000000000..787e3cdf0 --- /dev/null +++ b/services/mana-analytics/drizzle/0001_align-feedback-enums.sql @@ -0,0 +1,66 @@ +-- 0001_align-feedback-enums.sql +-- +-- Bringt die Postgres-Enums `feedback.feedback_category` und +-- `feedback.feedback_status` mit dem `@mana/feedback`-Package in Einklang +-- und legt die `onboarding-wish`-Kategorie für den letzten +-- Onboarding-Schritt an. +-- +-- Hand-authored, weil `drizzle-kit push` Enum-Werte nicht zuverlässig +-- umbenennt. Apply manually before next `pnpm db:push`: +-- +-- psql "$DATABASE_URL" -f services/mana-analytics/drizzle/0001_align-feedback-enums.sql +-- +-- Idempotent: alle Schritte verwenden `IF EXISTS` / `IF NOT EXISTS`-Checks +-- via DO-Blöcke, sodass Re-Runs nicht failen. + +BEGIN; + +-- 1. Status-Werte umbenennen, sodass sie zum Package passen. +-- PostgreSQL ≥10 supportet ALTER TYPE … RENAME VALUE non-destructive. +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON t.oid = e.enumtypid + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'new' + ) THEN + ALTER TYPE feedback.feedback_status RENAME VALUE 'new' TO 'submitted'; + END IF; + + IF EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON t.oid = e.enumtypid + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'reviewed' + ) THEN + ALTER TYPE feedback.feedback_status RENAME VALUE 'reviewed' TO 'under_review'; + END IF; + + IF EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON t.oid = e.enumtypid + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'done' + ) THEN + ALTER TYPE feedback.feedback_status RENAME VALUE 'done' TO 'completed'; + END IF; + + IF EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON t.oid = e.enumtypid + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'feedback_status' AND n.nspname = 'feedback' AND e.enumlabel = 'rejected' + ) THEN + ALTER TYPE feedback.feedback_status RENAME VALUE 'rejected' TO 'declined'; + END IF; +END +$$; + +-- 2. Default für status auf den neuen Wert setzen. +ALTER TABLE feedback.user_feedback ALTER COLUMN status SET DEFAULT 'submitted'; + +-- 3. Neue Category für Onboarding-Wishes anlegen. +ALTER TYPE feedback.feedback_category ADD VALUE IF NOT EXISTS 'onboarding-wish'; + +COMMIT; diff --git a/services/mana-analytics/src/db/schema/feedback.ts b/services/mana-analytics/src/db/schema/feedback.ts index 770d4895d..d1100b443 100644 --- a/services/mana-analytics/src/db/schema/feedback.ts +++ b/services/mana-analytics/src/db/schema/feedback.ts @@ -13,22 +13,27 @@ import { export const feedbackSchema = pgSchema('feedback'); +// Enum values must mirror @mana/feedback's FeedbackCategory / FeedbackStatus +// unions exactly. Renames or additions need a hand-authored SQL migration +// under services/mana-analytics/drizzle/ (drizzle-kit push can't safely +// rename enum values). export const feedbackCategoryEnum = pgEnum('feedback_category', [ 'bug', 'feature', 'improvement', 'question', 'praise', + 'onboarding-wish', 'other', ]); export const feedbackStatusEnum = pgEnum('feedback_status', [ - 'new', - 'reviewed', + 'submitted', + 'under_review', 'planned', 'in_progress', - 'done', - 'rejected', + 'completed', + 'declined', ]); export const userFeedback = feedbackSchema.table( @@ -40,7 +45,7 @@ export const userFeedback = feedbackSchema.table( title: text('title'), feedbackText: text('feedback_text').notNull(), category: feedbackCategoryEnum('category').default('other').notNull(), - status: feedbackStatusEnum('status').default('new').notNull(), + status: feedbackStatusEnum('status').default('submitted').notNull(), isPublic: boolean('is_public').default(true).notNull(), adminResponse: text('admin_response'), voteCount: integer('vote_count').default(0).notNull(), diff --git a/services/mana-analytics/src/services/feedback.ts b/services/mana-analytics/src/services/feedback.ts index 1ec1acaec..6f18a528f 100644 --- a/services/mana-analytics/src/services/feedback.ts +++ b/services/mana-analytics/src/services/feedback.ts @@ -20,6 +20,7 @@ export class FeedbackService { feedbackText: string; category?: string; title?: string; + isPublic?: boolean; deviceInfo?: Record; } ) { @@ -42,6 +43,10 @@ export class FeedbackService { title: title || data.feedbackText.slice(0, 80), feedbackText: data.feedbackText, category: (data.category as any) || 'other', + // Honor explicit isPublic from caller; otherwise let the column + // default (true) apply. Private intake categories like + // 'onboarding-wish' should pass `false`. + ...(typeof data.isPublic === 'boolean' ? { isPublic: data.isPublic } : {}), deviceInfo: data.deviceInfo, }) .returning();