New 'erinnerung' topic for manual Lyra community posts that gently remind
users they can add optional, anonymous profile details. Wording stays
jargon-free (no 'DiGA'/'data'/'study'). Manual-only, not in the auto-cron
catalog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MDM-VPN-Pivot (Phase F.2 done):
- ops/mdm/profiles/rebreak-iphone-protection.mobileconfig auf v5 mit
com.apple.vpn.managed Payload + OnDemandUserOverrideDisabled. iPhone-User
kann ReBreak-VPN-Profile nicht entfernen und "Bedarf verbinden"-Toggle
ist disabled. allowEnablingRestrictions empirisch widerlegt für FC-Toggle-
Lock — out.
- DEV-removable Variante als Test-Profile dazu.
- Bootstrap-Tool (rebreak-supervise.sh) + Supervision-Identity-Setup-Doc.
- PHASES.md updated mit empirischen Befunden.
App-side MDM-Detect (Pfad-a Banner-Logic):
- modules/rebreak-protection: getDeviceState() returnt mdmManaged via
Heuristik NETunnelProviderManager.count > 1 (App selbst kann nur einen
eigenen erstellen, MDM-Push fügt einen zweiten hinzu).
- DeviceLayers.mdmManaged?: boolean Type.
- blocker.tsx: lockedIn-Bedingung erweitert um mdmManaged. Bei MDM-managed
iPhones wird der App-Lock-Card (FC-Authorization-Toggle UI) ausgeblendet
weil der per-App FC-Toggle nicht lockbar ist und durch den MDM-VPN-Layer
redundant.
Layer-2-Country-Curated-Pivot:
- backend: vip-swap.post.ts raus, suggest.post.ts rein. Curated-domains
durch admin (separate Tabelle/Pfad), getrennt von User-Custom-Domains.
- Admin-APIs für curated-domain Pflege (index.get + [id].patch).
- seed-country-blocklists Script für initiale Curated-Domain-Liste.
- protection/webcontent-domains.get refactored für Country-Curated-Pfad.
- Migration drop_vip_swap_fields.sql + schema.prisma adjusted.
- docs/concepts/layer2-country-pivot.md mit Architektur + Decision-Trail.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-Bug: Admin-getriggerter Lyra-Post (Topic 'motivation' o.ä.) zeigte
trotz FR-locale im Frontend immer Deutsch. Root: LYRA_SYSTEM_PROMPT hatte
hartcodiert 'Auf Deutsch' → Groq output war immer DE.
Selbe Multi-Locale-Pattern wie approve.post.ts:
- 4 parallele Groq-Calls (Promise.allSettled) für de/en/fr/ar
- System-Prompt nimmt Lang-Parameter: 'Antwort AUSSCHLIESSLICH in <lang>'
- Content als JSON {de,en,fr,ar} gespeichert
- customContent-Path (Admin tippt selbst Text) bleibt plain — Admin schreibt
in seiner gewählten Sprache, PostCard zeigt allen Usern denselben Text
Frontend (PostCard.tsx) parsed JSON bereits richtig via
resolveLocalizedJsonContent (vorheriger Commit 44a3348).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: User mit FR-locale sahen Lyra-Confirmation-Posts trotzdem auf Deutsch
(Banner/Tabs richtig FR). Root: approve.post.ts generierte den Text via
Groq mit hartcodiertem 'auf Deutsch'-Prompt, speicherte als plain content.
Server (approve.post.ts):
- 4 parallele Groq-Calls (Promise.allSettled) — de + en + fr + ar
- Per-Locale-PROMPT_CFG mit subject/action/statsLine/thanksSegment-Texten
- Locale-aware Number-Format (toLocaleString('de-DE'|'en-US'|'fr-FR'|'ar-EG'))
- Content als JSON {de:'...',en:'...',fr:'...',ar:'...'} gespeichert
- Mindestens DE muss gelingen, sonst kein Post (Sicherheit gegen halbe Posts)
- ~4x Groq-cost pro Post (sehr günstig bei Llama-3.3-70b, parallel-latency
bleibt ähnlich)
Frontend (PostCard.tsx):
- resolveLocalizedJsonContent() — try-parsed JSON content
- Wenn JSON-Object mit Locale-Keys → pickt i18n.language, fällt auf DE → EN
- Sonst plain content (Legacy-Posts, Comments, User-Posts unverändert)
- Quick-Reject auf '{' first-char vermeidet JSON.parse-Overhead für 99.9%
der Text-Posts
Legacy-Posts in DB bleiben DE-only (kein retroaktiver Multi-Locale-Rewrite).
Neue Posts ab Deploy haben alle 4 Sprachen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User asked for the admin review tooling — and the lyra-bot community
post / notification text that goes out with each submission — to know
whether a submission is a website-domain or a mail-sender-domain. Until
now the type lived only on user_custom_domains and the submission
inherited it implicitly via the foreign key. Reading it back for the
admin list or the lyra prompt meant joining the source row every time.
- migration 20260516_domain_submission_type adds a type column to
rebreak.domain_submissions with a default of 'web' and backfills
every existing row from its linked user_custom_domains.type. The
backfill is idempotent (UPDATE … FROM with the type comparison).
- Composite index (type, status) so the admin pending-list can scope
by category without scanning the whole table.
- submitDomainForReview now copies the source row's type into the new
submission. The submit endpoint picks it up to vary the auto-generated
community-vote post copy: a website framing for type='web' and an
"Mail-Absender"-framing for type='mail_domain'. The user's nickname
is the only PII referenced.
- adminApproveSubmission returns the type alongside the domain so the
approve endpoint's Lyra-bot Groq prompt can swap its subject/action
labels per category. Reject path unchanged — the notification just
carries the bare domain string, no type framing needed.
- BlocklistDomain stays type-agnostic on purpose. The mail-daemon's
getBlocklistedDomainsSet is a flat string-set match against sender
domain or URL host, and works for both categories without splitting.
Adding a type there would be redundant work in v1.0 — revisit only
if we ever need a UI to surface what category each global entry
came from.
38/38 backend tests pass (8 admin/domains, 30 plan-limits including
5 new for the type-copy semantics and community-post text variants).
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>