POST /api/dev/set-plan { plan: 'free'|'pro'|'legend' } — requireUser, sets the
caller's own profile.plan via Prisma. Refuses on production URL (same guard as
the cooldown testMode: appUrl includes rebreak.org && !includes staging). Lets
the __DEV__ tier-toggle work without admin rights. Does NOT weaken updateProfile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- ProtectedDevice prisma model + migration add_protected_devices
- DB helpers: list/count/get/create/confirm/revoke
- mobileconfig.ts utility — XML-escape, unique UUIDs per request
- 5 endpoints under /api/devices/* (avoid /api/devices conflict with existing
Capacitor UserDevice route by using /api/devices/protected for list)
Phase 1: backend ready. DoH-server token-routing comes in phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wenn User App-Passwort aktualisiert via /api/mail/connect (upsert), waren bisher
lastConnectError + lastConnectErrorAt von der vorherigen Auth-Failure noch in
DB → /api/mail/status returned weiter Auth-Fehler-Status bis zum nächsten
IDLE-Heartbeat oder Cron-Scan diese überschrieb.
Jetzt: bei erfolgreichem Update räumt upsertMailConnection beide Felder, UI
zeigt sofort "Live" nach Passwort-Update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AdGuard Home auf rebreak-mdm pullt diese Liste alle 1h für DoH-DNS-NXDOMAIN.
Single source of truth mit dem URL-Filter (NEFilter) — gleicher
getActiveBlocklistDomains() backend-call.
Public (no auth) — Casino-Domains sind keine PII, andere DNS-Blocklisten
(HaGeZi, OISD) sind genauso public.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 3 fields to mail_connections so UI can distinguish between
"connection alive but no new mail" vs "connection dead" vs "auth-failed":
- last_connect_error — text of last IMAP error (auth-fail, connect-fail)
- last_connect_error_at — timestamp of error
- last_idle_heartbeat_at — updated every 2min by NOOP-success in daemon
Daemon (backend/imap-idle/index.mjs):
- updateConnectionError() / clearConnectionError() / updateIdleHeartbeat()
SQL helpers
- logError now uses err.responseText (shows "AUTHENTICATIONFAILED" instead
of generic "Command failed")
- clearError on connect() success
- updateError on connect() catch
- updateHeartbeat in NOOP-success-path (every 2min)
API (status.get.ts): returns the 3 new fields per account.
Migration: ALTER TABLE rebreak.mail_connections ADD COLUMN ... (idempotent).
UI-side (in flight, separate task): MailAccountCard renders auth-error
banner when lastConnectError != null + heartbeat-based "live" indicator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds optional `gameName` column to community_posts so game-share posts
can render with the game-banner above the post-content (Snake/Tetris/
Memory/TTT visual indicator).
- prisma/schema.prisma: CommunityPost.gameName String? @map("game_name")
- migration: ALTER TABLE rebreak.community_posts ADD COLUMN game_name
- db/community.ts: createPost() accepts gameName param
- api/community/post.post.ts: extracts gameName from body
- api/community/posts.get.ts: returns gameName, prefers DB over content-parse
Frontend (already in flight on upgrade/sdk-54): PostCard.tsx renders
GameShareBanner when post.category === 'game_share' && post.gameName.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nitro auto-import doesn't reliably pick up named exports from
db/voiceQuota.ts at runtime — speak endpoint threw 500 with
"ReferenceError: getRemainingVoiceQuota is not defined".
Explicit imports for getRemainingVoiceQuota, consumeVoiceQuota,
estimateAudioSeconds + getPlanLimits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LLMs (especially Haiku) keep emitting markdown despite explicit "no markdown"
prompt rule. Mobile app has no markdown renderer — users see raw asterisks.
- New stripMarkdown() util handles **bold**, bullet-lists, headings,
code-fences, links, blockquotes
- /api/coach/message: applies stripMarkdown(text) post-LLM as safety-net
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per strategist-spec: Lyra-Coach-Mode klarer von SOS-Mode trennen.
- SOS-Mode (urge): crisis-intervention, focused, kurz
- Coach-Mode (lyra): casual, profile-building, philosophy, features
Backend (backend/server/api/coach/message.post.ts):
- COACH_CASUAL_SYSTEM_PROMPT komplett neu strukturiert (~620 tokens)
- Stärkerer Fokus: 3 explicit Aufträge (echtes Gespräch / Profile-Building /
Rebreak sprechen)
- Profile-building-mandate: "wenn du wenig weißt, sag's ehrlich; frag nach
Hobbies/Zielen/Menschen — eingewoben, NICHT als Checkliste"
- Cleanere Mission-Section: Bewegung, Anonymität, kein-pathologisieren,
community-getrieben, DiGA-Listung-Ziel
- Hard-rules klarer: NIE demographics extrahieren (User-Form ist tabu),
kein Sucht-Vokabular, kein medical-advice
- Existing PLAN_DETAILS-template-var bleibt
- Memory-system unverändert (lyra-memories table, extractAndStoreMemories
fire-and-forget — kein schema-change nötig)
Frontend Mode-Badges:
- app/lyra.tsx (Coach-Mode): Header-pill "Coach" in brandOrange-tint neben
Lyra-name
- app/urge.tsx (SOS-Mode): Header-pill "SOS" in error/red-tint neben
Lyra-name (alt: "Lyra · SOS [v2]" inline-text → cleaner badge-style)
i18n:
- coach.modeBadge.coach + coach.modeBadge.sos in DE + EN
Switch-Logic: route-based (lyra.tsx vs urge.tsx → separate persona via
backend endpoint). Kein User-Toggle — User soll nicht entscheiden müssen
"bin ich grade in Krise?".
Implementation Risk: LOW — schema-neutral, prompt-only + 2 small UI badges.
Erste Beta-Testing-Phase: ~1-2 Wochen iterieren bei Feedback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend-side admin-auth. Admin-App (apps/admin/) braucht das damit
useAdminAuth.verifyAdminRole() nach Login server-side prüfen kann ob User
in admin_users-tabelle steht.
New schema:
- model AdminUser → table rebreak.admin_users (user_id UUID PK FK Profile.id,
created_at, added_by). Migration 20260508_admin_users/migration.sql.
- ⚠️ SCHEMA-MIGRATION — NICHT autopushen. User entscheidet wann pipeline
triggert.
New backend code:
- backend/server/db/admin.ts: isAdminUser(userId) → boolean
- backend/server/utils/auth.ts: requireAdmin(event) wraps requireUser +
isAdminUser-check. Throws 403 wenn nicht admin.
- backend/server/api/admin/verify-admin.get.ts: GET endpoint. Returns
{ isAdmin: true, userId, email } bei success, 403 sonst, 401 if not auth'd.
Tests (5 cases in tests/admin/verify-admin.test.ts):
- isAdminUser DB-layer: row exists/null
- requireAdmin: admin → user, non-admin → 403, no token → 401
- Endpoint: admin → success, non-admin → 403
Pending User-Actions nach Push+Deploy:
1. Migration deploy auf staging:
ssh rebreak-server && cd /srv/rebreak && pnpm exec prisma migrate deploy
2. Seed-Admin eintragen:
INSERT INTO "rebreak"."admin_users" ("user_id", "created_at")
VALUES ('128df360-2008-4d6f-8aa1-bdb41ec1362f', NOW())
ON CONFLICT DO NOTHING;
3. Admin-App composables/useAdminAuth.ts kann dann verifyAdminRole()
gegen GET /api/admin/verify-admin aufrufen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Endpoint /api/social/profile/[userId] returned (profile as any).postsCount ?? 0
und (profile as any).followingCount ?? 0 — Profile-schema hat aber weder
postsCount noch followingCount columns. Daher zeigte UI immer 0 obwohl User
Posts hatte.
Fix: 2 zusätzliche COUNT-queries in Promise.all:
- usePrisma().communityPost.count({ userId, isModerated: false }) → postsCount
- usePrisma().userFollow.count({ followerId: userId }) → followingCount
followersCount bleibt unverändert (wird via trigger denormalisiert in profile-row).
Tests: backend/tests/social/profile-counts.test.ts — 4 Cases
(posts>0, posts=0, following count, followers passthrough). 4/4 grün.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coach-Page ist NICHT SOS — User ist nicht in Krise, will small-talk,
Reflexion, Feature-Wuensche, Philosophie. „Lockere Lyra".
Aenderungen:
- Neuer Prompt COACH_CASUAL_SYSTEM_PROMPT (exportiert): warm/neugierig/
manchmal humorvoll, bis 4-5 Saetze, darf eigene Empfehlungen + Mini-
Meinungen formulieren, lädt Feedback aktiv ein. Kein Crisis-Framing.
Sprachregeln (keine Pathologisierung, kein „Sucht") gelten unveraendert.
- Tier-LLM-Routing analog zu sos-stream:
Free/Pro = Groq llama-3.3-70b-versatile (Fallback llama-3.1-8b)
Legend = OpenRouter anthropic/claude-haiku-4.5 (Fallback claude-3.5-haiku)
- max_tokens 280→500 (Coach darf laenger antworten)
- Demographics-Injection (analog sos-stream): birthYear/gender/etc als
USER-DEMOGRAPHIE-Block in Prompt (read-only, kein Extract)
- sosMode-Branch deprecated — Frontend kann den Param noch senden, wird
ignoriert. Folge-TODO: UI-Agent entfernt sosMode aus Coach-Call.
NICHT geändert:
- TTS-Endpoints bleiben plan-agnostisch (Frontend routet by tier)
- sos-stream.get.ts/sos-stream.post.ts unberuehrt (importieren weiter
COACH_SYSTEM_PROMPT, kein Breaking Change)
Memory:
- project_llm_per_plan.md (tier-LLM-Default-Logic)
- feedback_anonymity_nickname.md
- feedback_demographics_user_initiated.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
let profile vor try-block hoisten, damit es im plan-routing-Fallback
(line 210, profile?.plan) sichtbar ist. Vorher: const profile innerhalb
try-block → block-scoped → ReferenceError außerhalb.
Demographics-Block-Injection added — gracefully no-op wenn neue Felder
(birthYear/gender/etc.) noch nicht im DB-Schema sind (optional chaining).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lyra halluzinierte Namen wie 'Max' im SOS-Stream weil systemPrompt
keinen Nicknamen-Anker hatte. message.post.ts hatte das Pattern schon,
aber beim Cutover ins backend/-Layout war's nur dort, nicht in
sos-stream.{get,post}.ts.
Fix: getProfile(user.id) + Inject 'NUTZER-NAME: ...' wie in message.post.ts.
Non-blocking (catch), Memory-Pattern preserved.
Im Cutover wurde 'const key = config.openrouterApiKey' rausgenommen,
aber line 311 referenzierte 'key' weiter für extractAndStoreMemories.
ReferenceError fiel in catch → 'stream failed' SSE event → App
fiel auf non-streaming fallback (4-5s wait) statt streaming.
Fix: explizit memoryExtractKey aus config.openrouterApiKey holen.