refactor(feedback): align package + DB enums, plan central hub

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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-26 21:52:25 +02:00
parent bf3bca268a
commit ba6274edbe
6 changed files with 339 additions and 7 deletions

236
docs/plans/feedback-hub.md Normal file
View file

@ -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 `<ModuleFeedbackHook module="todo" />` 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.

View file

@ -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<string, unknown>;
}

View file

@ -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<FeedbackCategory, string> = {
feature: 'Feature',
improvement: 'Verbesserung',
question: 'Frage',
praise: 'Lob',
'onboarding-wish': 'Was ich mir wünsche',
other: 'Sonstiges',
};

View file

@ -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;

View file

@ -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(),

View file

@ -20,6 +20,7 @@ export class FeedbackService {
feedbackText: string;
category?: string;
title?: string;
isPublic?: boolean;
deviceInfo?: Record<string, unknown>;
}
) {
@ -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();