273 Commits

Author SHA1 Message Date
chahinebrini
c2ad1a1780 docs(pricing): capture conversion philosophy (membership/community framing, not frustration-gating)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:55:36 +02:00
chahinebrini
2dcff6408c feat(chat): redesign chat list + conversation view
- RoomCard / chat.tsx DmItem: cleaner list rows (48px avatar, minHeight 68,
  consistent padding, time next to name, TouchableOpacity)
- ChatBubble: timestamp inline under content (no absolute-position hack),
  borderRadius 20, 28px avatar, lighter backdrop
- ChatInput: surface bg, hairline-bordered input pill, 38px send button

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:52:45 +02:00
chahinebrini
7369912d60 feat(dev): switch plan-override to POST /api/dev/set-plan + add Settings debug row
debug.tsx: removed admin-403 special-case, calls /api/dev/set-plan directly.
settings.tsx: new PlanPickerSheetContent (TrueSheet, DEV-only) in debug section
with three plan options; uses same endpoint + invalidateMe().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:51:14 +02:00
chahinebrini
5c9f3f687f docs(pricing): §0.5 Rev.2 — incorporate accepted strategist critique
free gets a curated core blocklist (~1-2k) instead of nothing; ≤24h SLA →
'usually 1-2 business days' + auto-approve fallback; Legend column presentation
+ PR one-liner + Phase-2 impl points (curated list, daemon gating, auto-approve,
slot-frees-on-approval).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:51:11 +02:00
chahinebrini
a8e638ed88 feat(profile): replace system-crop with custom gesture-based AvatarCropSheet
Picker now uses allowsEditing:false + quality:1; picked URI routes through
AvatarCropSheet (Pinch+Pan via RNGH+Reanimated, square crop frame with
corner markers). manipulateAsync crop left as TODO — expo-image-manipulator
not yet installed; sheet passes URI through unchanged until then.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:51:09 +02:00
chahinebrini
3da76bcb15 docs(pricing): §7 — strategist's marketing critique of the final plan
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:49:50 +02:00
chahinebrini
e93f580f3b docs(pricing): final tier decision (User 2026-05-11) + open impl points
free: 5 custom domains (non-refillable), no global blocklist, 1 mail account,
basic coach. pro: global blocklist (the headline), 5 refillable domains, 3 mail
accounts (cron choice, no daemon), better coach. legend: + IMAP-IDLE daemon
(real-time mail scan — app highlight), 10 refillable domains with ≤24h ReBreak
validation, much better coach, +2 device DNS profiles. Marketing critique (§7)
pending from strategist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:44:26 +02:00
chahinebrini
eb871073f2 feat(backend): __DEV__ /api/dev/set-plan — user sets own plan (non-prod only)
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>
2026-05-11 15:44:26 +02:00
chahinebrini
14452b2a46 refactor(native): Pressable → TouchableOpacity sweep (style-fn swallows Android styles)
Alle <Pressable style={({pressed}) => ({...})}> ersetzt — style-Funktion
droppt auf Android (New Arch) intermittierend width/height, führt zu 0×0
unsichtbaren Elementen. TouchableOpacity mit activeOpacity ist stabil.

Außerdem übrige Pressables (plain style) aus components/ und app/
migriert sowie zwei überschüssige </View>-Tags in chat.tsx + RoomCard.tsx
entfernt die TS-Fehler verursacht haben.

64 Dateien, typecheck sauber.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:43:10 +02:00
chahinebrini
1ad964f54b fix(backend): alias NUXT_*_BOT_USER_ID env vars in start-staging.sh
Infisical staging holds the bot user IDs as NUXT_LYRA_BOT_USER_ID /
NUXT_REBREAK_BOT_USER_ID, but nitro.config.ts reads process.env.LYRA_BOT_USER_ID
(no NUXT_ fallback) and start-staging.sh had no alias for them → config.lyraBotUserId
was empty → POST /api/admin/lyra-post threw 500 "LYRA_BOT_USER_ID nicht
konfiguriert" (surfaced via the admin app proxy). Adds the alias + NITRO_ override
exports, same pattern as the other keys.

Also: ops/strategy/pricing-tiers.md — strategist's tier-pricing analysis,
stress-test, downgrade-policy matrix, plan-change briefing-screen content +
scenario test matrix (Task #8 Phase 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:37:38 +02:00
chahinebrini
f6852be760 fix(native): useUserPlan derive from useMe (was a stale module cache)
useUserPlan had its own module-level cache + fetch-once-on-mount, separate
from useMe's invalidateMe(). So the __DEV__ tier-override toggle (which calls
invalidateMe()) never reached useUserPlan consumers → the app didn't react to
a plan switch. Now useUserPlan just reads me.plan from useMe → inherits its
live-invalidation, the toggle propagates everywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:18:59 +02:00
chahinebrini
790b5e5c24 feat(admin): surface Lyra-Posts page in nav + dashboard grid
The pages/lyra.vue page (create community posts as Lyra/ReBreak, AI-generated
or manual) existed but wasn't linked anywhere. Adds it to the sidebar +
mobile bottom-tab (grid-cols-5→6) and the dashboard quick-links grid
(lg:grid-cols-4→5). Admin app stays team-internal (stats / users / domain
approval / social posts / moderation) — no relation to the RN app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:06:12 +02:00
chahinebrini
297ad7437b fix(android): adaptive icon — white bg + padded dark logo (was zoomed/clipped)
Old adaptiveIcon was a full-bleed dark logo on a #0a0a0a background → the
launcher mask cropped it ("zoomed in" look). Now: white background (matches
the Play Store listing icon look) + the same logo at ~62% on a transparent
canvas → mask has nothing to clip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:06:12 +02:00
chahinebrini
c6a4b04383 chore(deps): pin metro-* family to 0.83.3 via pnpm.overrides
Fixes the metro-symbolicate version skew (nested 0.82.5 copies under
metro@/metro-source-map@) that broke the release JS bundle step
("SourceMetadataMapConsumer is not a constructor" in composeSourceMaps).
Required a clean reinstall (rm -rf node_modules && pnpm install) to drop
the stale nested dirs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:06:12 +02:00
chahinebrini
385f0b42a9 fix(android): blocker toggles + invisible avatar + adaptive icon
- protection.ts: normalize Android device-state keys (vpn/accessibility/
  tamperLock) to the iOS-shaped names the UI reads (urlFilter/familyControls/
  appDeletionLock) — on Android the layers came back under different keys, so
  blocker.tsx saw all toggles as undefined → always off → optimistic toggle
  flipped back to off after enabling
- AppHeader.tsx: avatar/bell/back Pressable-with-style-fn → TouchableOpacity
  with plain style — style-fn was swallowing width/height on Android → 0×0
  + overflow:hidden → avatar invisible (same pattern as Mac-CTA fix 7d04e42)
- app.config.ts: adaptiveIcon.foregroundImage → padded adaptive-foreground.png
  (logo in ~66% safe zone, was full-bleed → clipped by launcher mask);
  icon → icon.png (clean 1024 opaque, was the 512px alpha variant)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:52:42 +02:00
chahinebrini
e9d34dbe78 feat(settings): subscription section + __DEV__ plan-override toggle
- settings.tsx: real "Abo" row showing current plan (Free/Pro/Legend badge),
  taps open a sheet explaining subscriptions are managed on rebreak.org
  (Linking.openURL → /account; TODO: gate for iOS App-Store submission per
  Apple 3.1.1 — no in-app purchase flow)
- debug.tsx: __DEV__-only plan-override toggle (free/pro/legend) via
  PATCH /api/admin/users/:id + invalidateMe(); shows admin-only hint on 403
- locales: settings.subscription_* keys (de + en)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:13:47 +02:00
chahinebrini
bcc6e5cba1 chore(android): versionCode 2, eas.json profiles, avatar PNG fix
- app.config.ts: android.versionCode 2 (was 1)
- eas.json: development/preview/production profiles, EXPO_PUBLIC_API_URL=staging,
  appVersionSource=local, autoIncrement=false
- avatars: switch DiceBear endpoint /svg -> /png — RN <Image> can't decode SVG,
  Hero-Avatars rendered transparent/blank on Android

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:00:02 +02:00
chahinebrini
dd3d8c6667 feat(devices): wire Windows DoH AddWindowsSheet into devices screen
- AddWindowsSheet: 5-step Lyra flow (download → datei → shield-check → wifi → done)
- devices.tsx: Windows button enabled, opens AddWindowsSheet
- protectedDevices store: enroll() takes platform 'mac' | 'windows'
- AddMacSheet: pass 'mac' to enroll()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:44:41 +02:00
chahinebrini
518510c088 feat(mail): IONOS-Detection + MX-Lookup-Fallback + humanisierte Error-Messages
- imap-providers: IONOS/1&1/1blu, msn.com, magenta.de, yahoo.co.uk, ymail.com, tutanota hinzugefügt
- detectImapProviderAsync: MX-Lookup-Fallback für Custom-Domains (IONOS kundenserver.de/ionos.de Pattern)
- connect.post.ts: nutzt jetzt detectImapProviderAsync statt sync-Variante
- ConnectMailSheet: rohe Server-Errors werden via humanizeMailError() + t() übersetzt
- useMailConnect: IONOS/t-online/freenet Domains in Client-Side-Detection ergänzt
- Locale de/en: provider_other, app_password_guide_other, host_unreachable, unknown Text präzisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 05:15:29 +02:00
chahinebrini
6962e09403 feat(devices): Windows 11 DoH protection — reg-file endpoint + tests
- Add server/utils/regfile.ts: generateWindowsDohRegFile() producing
  UTF-16 LE + BOM .reg content for DohWellKnownServers registry path.
  label and dohTemplate values are properly escape'd (\, ", \n, \r, \t).
- Add GET /api/devices/:id/profile.reg — public, windows-platform-gated,
  returns octet-stream with Content-Disposition attachment.
- Update enroll.post.ts: downloadUrl is now platform-aware
  (windows → .reg, all others → .mobileconfig).
- Add tests/devices/regfile.test.ts: 13 tests covering BOM, CRLF,
  token embed, subkey naming, AutoUpgradeFlag, label escaping (", \, \n),
  and labelToSlug edge cases. All 111 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 04:48:51 +02:00
chahinebrini
58287f206d fix(realtime): enable Supabase Realtime publication for rebreak.notifications
Fixes [notifRealtime] CHANNEL_ERROR — table was not in supabase_realtime
publication, so postgres_changes events never arrived. Added by backyard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:40:32 +02:00
chahinebrini
7d04e42bb5 fix(devices): Mac CTA-button invisible — Pressable style-fn → TouchableOpacity
Memory feedback_known_ui_layout_bugs.md Pattern 5: Pressable with
style={({pressed}) => ({...})} is layout-poison in some RN-render-paths,
button collapses to 0-height and renders invisible. Windows-button right
below worked because it uses static style={{...}}.

TouchableOpacity gets same press-feedback via activeOpacity prop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:38:26 +02:00
chahinebrini
eccc04b1e3 fix(android): generate missing a11y service resources in plugin
Plugin referenced @string/accessibility_service_summary +
@xml/accessibility_service_config in AndroidManifest but never created the
underlying resource files. EAS Cloud prebuild --clean exposed this — local
dev worked because resources were sometimes already there from previous builds.

- withStringsXml: adds accessibility_service_summary string (DE)
- withDangerousMod: writes res/xml/accessibility_service_config.xml at prebuild
- Config flags match native service (TYPE_WINDOW_CONTENT_CHANGED + STATE_CHANGED,
  canRetrieveWindowContent for URL-bar reading)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:32:16 +02:00
chahinebrini
5eebda4b6b fix(android): bump compileSdk + targetSdk to 36 for androidx.core 1.17
EAS Cloud prebuild ignores local android/build.gradle pins (android/ is gitignored).
Plugin compileSdk 35 → 36 satisfies new androidx.core dependency requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:09:45 +02:00
chahinebrini
6700391eed feat(devices): Settings → Geräte UI + AddMacSheet 3-step Lyra flow
Frontend:
- New devices.tsx: section 'this device' + 'protected devices' + Legend CTA
- AddMacSheet: label → Lyra-onboarding (4 steps) → success
- protectedDevices store (Zustand): load, enroll, confirmInstalled, remove
- Locale strings DE + EN (devices namespace, 36 keys each)
- Path-fix: /api/devices/protected (was /api/devices) + /api/devices/:id/revoke

Free/Pro see upgrade-CTA, Legend see add-Mac. Windows button shown disabled (soon).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:06:49 +02:00
chahinebrini
677b67902b feat(devices): protected device enrollment + mobileconfig generator
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>
2026-05-11 04:06:49 +02:00
chahinebrini
3088526fc1 feat(icon): use rebreak_android.png from Play Console as app icon
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:43:31 +02:00
chahinebrini
d9c41d4427 fix(deps): patch metro-core to expose ./src/* for metro-cache 0.82.5 nested
metro-cache 0.82.5 (nested under metro 0.82.5) imports metro-core/src/canonicalize
directly. Top-level metro-core 0.83.3 has restrictive exports map that blocks this.
Pnpm patch adds ./src/* to exports while preserving the existing ./private/* path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:41:33 +02:00
chahinebrini
ee25a50288 chore(eas): add Expo projectId to app.config.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:34:31 +02:00
chahinebrini
2e6785e5a3 merge: integrate upgrade/sdk-54 into main
Reconciles 20 sdk-54 commits with 10 main commits.

Mobile (Expo):
- KeyboardAwareSheet migrations + Snake/Tetris UI + Lyra-feedback game-over flow
- Dark Theme system (Wave 1 + Wave 2 — global color-tokens)
- Profile Avatar + Nickname edit-flow
- Sound system for games (useSnakeSounds)
- Best-score persistence + share-to-community

Admin (Nuxt):
- Phase 2 backend (Users + Moderation endpoints + 2 schema migrations)
- Phase 2 frontend (Domains/Stats/Users/Moderation pages, responsive layout)
- Lyra-Posts feature migration from legacy nuxt-rebreak

Backend (kept main's versions for stability):
- IMAP IDLE-daemon: GMX silent-drop fix (10min renew, 2min NOOP heartbeat)
- mail/status: connect-error tracking + heartbeat fields
- coach/speak: explicit voice-quota helper imports
- prisma: preserved gameName field (production DB column exists)

Conflict resolutions:
- apps/admin/pages/index.vue: theirs (sdk-54 adds Lyra-Posts quick-link)
- apps/rebreak-native/app/lyra.tsx: theirs (Dark-Theme color binding)
- locales/de.json + en.json: theirs (game-rating + share strings)
- GameOverScreen.tsx: theirs (full new feature, 505 vs 256 lines)
- UrgeGames.tsx: theirs (consistent with new GameOverScreen props)
- backend/imap-idle/index.mjs: ours (preserves GMX-fix + heartbeat)
- backend/prisma/schema.prisma: ours (preserves gameName field on prod DB)
- backend/server/api/coach/speak.post.ts: ours (explicit imports fix)
- backend/server/api/mail/status.get.ts: ours (cleaner without type-cast)
- apps/admin/start-admin-staging.sh: ours (preserves PORT-3017 override fix)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:24:32 +02:00
chahinebrini
b1b3b5eb36 feat(admin): migrate lyra-posts feature from legacy nuxt-rebreak
- Add apps/admin/pages/lyra.vue — LLM-generierter oder manueller Bot-Post als Lyra/ReBreak
- Add apps/admin/server/api/admin/lyra-generate.post.ts — Proxy zu backend
- Add apps/admin/server/api/admin/lyra-post.post.ts — Proxy zu backend
- Add apps/admin/server/api/admin/lyra-profile.get.ts — Proxy zu backend
- Add apps/admin/server/api/admin/set-lyra-avatar.post.ts — Proxy zu backend
- Update apps/admin/pages/index.vue — Lyra-Posts Quick-Link auf Dashboard
Auth via admin-auth Middleware + server-side adminSecret Proxy-Pattern.
BenAvatar (Rive, legacy) entfernt, Avatar-Anzeige bleibt via lyra-profile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 02:11:51 +02:00
chahinebrini
2d0983d6c8 fix(mail): clear lastConnectError on successful re-connect
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>
2026-05-11 00:00:33 +02:00
chahinebrini
5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow
mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail.
GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift.

- KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding
- react-native-keyboard-controller installiert + KeyboardProvider in Root
- Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO)
- Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn
- DPad-Buttons 60→48, more bg, no scale
- useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates
- dm.tsx: resolveAvatar wrap (iron.png-Warning)
- Mail-error-humanizer + locales

Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:59:25 +02:00
chahinebrini
6afffdbb18 fix(mail): clear connect-error on re-connect + return error fields in status
- upsertMailConnection: bei Update lastConnectError + lastConnectErrorAt auf
  null — User aktualisiert App-Passwort → UI zeigt sofort wieder Live (statt
  stale Auth-Fehler-Status bis nächstem IDLE/Scan-Cycle)
- /api/mail/status: liefert lastConnectError, lastConnectErrorAt,
  lastIdleHeartbeatAt mit (waren bisher nicht im Response → Frontend hat den
  Status nie korrekt rendern können)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:58:05 +02:00
chahinebrini
86445d8607 feat(url-filter): add blocklist.txt endpoint for DNS-filter sync
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>
2026-05-10 16:18:11 +02:00
chahinebrini
347ad1f6c5 feat(url-filter): add blocklist.txt endpoint for DNS-filter sync
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>
2026-05-10 16:17:24 +02:00
chahinebrini
c1a66e3d07 feat(mail): connect-error tracking + IDLE-heartbeat for accurate UI status
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>
2026-05-09 23:48:11 +02:00
chahinebrini
01420eaa09 fix(imap-idle): IDLE-renew 25min→10min + NOOP-heartbeat (GMX silent-drop fix)
User-test: Casino-mail an Chahine@gmx.net wurde nicht geblockt obwohl
daemon "connected" zeigte. Mo's diagnose: GMX dropped IDLE-connection
silent (kein TCP-error, kein logout). ImapFlow.idle() hängt unbegrenzt
ohne reject — exists-events kommen nie an, daemon ist faktisch tot.

2 Fixes:
1) IDLE_RENEW_INTERVAL_MS: 25 min → 10 min. GMX timeout-window ist
   ~10-15min, 25min war zu lang. Trade-off: alle 10min full reconnect.
2) NOOP-heartbeat alle 2min während IDLE-loop. Wenn NOOP fail
   (= silent-drop detected) → close → reconnect-loop. Early-detection.

Andere provider (Gmail/iCloud/Outlook) sind unaffected — die haben
~29min IDLE-timeout, also passt 10min auch dort safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:42:09 +02:00
chahinebrini
a81ba2e54a feat(community): Post.gameName + GameShareBanner-rendering chain
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>
2026-05-09 22:28:07 +02:00
chahinebrini
fd737f8658 fix(imap-idle): use snake_case table + columns (match Prisma @map)
Daemon SQL used PascalCase "MailConnection" + camelCase column-names
that match the Prisma model field-names — but actual DB has snake_case
table "mail_connections" with snake_case columns (per @map decorators).

Result: daemon was online but ALL queries failed with
  relation "rebreak.MailConnection" does not exist
→ no mailboxes loaded → no IDLE-sessions established.

Fix: query "rebreak.mail_connections" with snake_case columns, alias
back to camelCase via SQL AS so rest of the daemon code works unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:57:14 +02:00
chahinebrini
343a25bc05 fix(deploy): scp imap-idle mit Punkt-Notation (kein Unterverzeichnis-Bug)
scp -r imap-idle/ target/ erstellt imap-idle/imap-idle/ wenn target existiert.
Fix: imap-idle/. kopiert Inhalt direkt in target ohne extra Subdir.
Plus: rm -rf + mkdir vor scp fuer idempotente Deploys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:54:30 +02:00
chahinebrini
d0d12dd3b2 fix(deploy): scp imap-idle mit Punkt-Notation (kein Unterverzeichnis-Bug)
scp -r imap-idle/ target/ erstellt imap-idle/imap-idle/ wenn target existiert.
Fix: imap-idle/. kopiert Inhalt direkt in target ohne extra Subdir.
Plus: rm -rf + mkdir vor scp fuer idempotente Deploys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:54:26 +02:00
chahinebrini
81a5f128e5 fix(deploy): mkdir -p imap-idle dir vor scp (first-deploy path fehlt)
scp -r schlaegt fehl wenn Zielverzeichnis nicht existiert.
Loest den GH-Actions-Fehler "realpath /srv/rebreak/backend/imap-idle/: No such file".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:50:51 +02:00
chahinebrini
062e542519 fix(deploy): mkdir -p imap-idle dir vor scp (first-deploy path fehlt)
scp -r schlaegt fehl wenn Zielverzeichnis nicht existiert.
Loest den GH-Actions-Fehler "realpath /srv/rebreak/backend/imap-idle/: No such file".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:50:44 +02:00
chahinebrini
de701677b2 feat(mail): IDLE-daemon for real-time Legend mail-protection
Standalone ESM-daemon that:
- Connects via ImapFlow IDLE to all active Legend mailboxes
- Triggers /api/mail/scan-internal on new-mail events (real-time)
- Auto-renew IDLE every 25min (RFC 3501 limit), exponential-backoff reconnect
- DB-refresh every 5min for new/removed connections

Plus deploy-pipeline:
- GH-Actions artifact-upload + scp to /srv/rebreak/backend/imap-idle/
- npm install --production on server (imapflow + pg)
- pm2 startOrReload via ecosystem.config.js
- start-idle-staging.sh wrapper for Infisical secret-injection

Replaces 30min-cron polling for Legend tier -- Casino-mails now blocked
within seconds, fulfilling Legend tier marketing promise.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:48:44 +02:00
chahinebrini
a24a9c783f feat(mail): IDLE-daemon for real-time Legend mail-protection
Standalone ESM-daemon that:
- Connects via ImapFlow IDLE to all active Legend mailboxes
- Triggers /api/mail/scan-internal on new-mail events (real-time)
- Auto-renew IDLE every 25min (RFC 3501 limit), exponential-backoff reconnect
- DB-refresh every 5min for new/removed connections

Plus deploy-pipeline:
- GH-Actions artifact-upload + scp to /srv/rebreak/backend/imap-idle/
- npm install --production on server (imapflow + pg)
- pm2 startOrReload via ecosystem.config.js
- start-idle-staging.sh wrapper for Infisical secret-injection

Replaces 30min-cron polling for Legend tier -- Casino-mails now blocked
within seconds, fulfilling Legend tier marketing promise.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:48:33 +02:00
chahinebrini
e5c9fadd1d fix(speak): explicit imports for voice-quota helpers
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>
2026-05-09 18:11:42 +02:00
chahinebrini
30ed4191b6 fix(coach): markdown-strip safety-net for LLM responses
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>
2026-05-09 17:58:22 +02:00
chahinebrini
f00d2319a5 fix(coach): markdown-strip safety-net + tier-aware speak-endpoint
Backend:
- New stripMarkdown() util (utils/strip-markdown.ts) — handles **bold**,
  bullet-lists, headings, code-fences, links, blockquotes
- /api/coach/message: applies stripMarkdown(text) post-LLM as safety-net
  because Haiku/Llama keep emitting markdown despite explicit prompt rule

Frontend:
- lyra.tsx voice-flow: hardcoded /api/coach/speak-openai → /api/coach/speak
  (tier-aware dispatcher: Free=Google, Pro=Cartesia, Legend=ElevenLabs)
- Added Metro debug-logs at TTS call-site for endpoint + status visibility
- detectEmotion extracted to lib/lyraResponse.ts (was inline duplicate)
- RiveAvatar: small type-export adjustment for shared Emotion type

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:54:34 +02:00
chahinebrini
b40b8465b9 feat(lyra,voice): founder-story + voice-tier-mapping + quota system
Two features in one push (both backend, deploy together):

LYRA FOUNDER-STORY (per strategist Option C — mixed/medium-detail):
- COACH_CASUAL_SYSTEM_PROMPT: GRÜNDER-STORY sub-block
  - Sharing-rules: ALWAYS on direct ask, RARELY proactive (only on
    explicit isolation expressions "niemand versteht das"), NEVER in
    SOS-mode, NEVER first-3-msgs, NEVER if user appears minor
  - Detail-level: "aus persönlicher Erfahrung mit Spielsucht in seiner
    Familie" — KEINE Namen, Verwandtschaftsgrade, Verlust-Details
  - Post-share-pivot: "...aber jetzt zu dir: was ist gerade los?"
- COACH_SYSTEM_PROMPT (SOS): SOS-MODE LOCK — hard-Verbot Gründer-Story
  zu erwähnen, auch bei direct-ask. Re-trigger-Risk zu hoch.
- DSGVO: brother bleibt komplett anonymisiert. Hans-Müller-DSB-review für
  verbal-consent-doc empfohlen.

VOICE TIER-MAPPING (per user-decision: voice für ALLE tiers):
- New plan-features.voice config: provider + model + voiceId + dailyQuotaSeconds
- Tier-mapping:
  - Free  → Google TTS Neural2-F (de-DE), 60s/day,  ~$4/1M chars
  - Pro   → Cartesia Sonic-2,            300s/day,  ~$4/1M chars + ~75ms TTFT
  - Legend → ElevenLabs Turbo v2.5,      unlimited, ~$30/1M chars
- New backend/server/db/voiceQuota.ts:
  - getRemainingVoiceQuota(userId, plan)
  - consumeVoiceQuota(userId, seconds)
  - estimateAudioSeconds(text)
- speak.post.ts komplett umgeschrieben als plan-aware dispatcher
- 14 tests passing (partial-consume, exhausted, day-rollover, edge-cases)
- Schema-migration 20260509_voice_quota:
  ADD voice_seconds_used_today, voice_quota_reset_at to profiles
  (auto-deploy via pipeline)

Pending Frontend (separate task):
- Voice-quota-UI in Settings/Profile (remaining seconds + upgrade-prompt
  bei 429 quota_exceeded)

⚠️ Schema-migration auto-deploy via b38bf17 detection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:28:36 +02:00