Commit graph

898 commits

Author SHA1 Message Date
Till JS
0fc16d1bfd feat(articles): bulk-import AI tool wiring (Phase 6)
Adds import_articles_from_urls tool to the articles module so the AI
Workbench can kick off a bulk-import job in one call. Auto-policy: the
job itself is the unit of approval, no per-article propose card.

- shared-ai schemas: declare the tool name + propose/auto policy
- articles/tools.ts: implement parseUrls + articleImportsStore.createJob
- consume-pickup.ts: handle the new event type
- events/catalog.ts: register article-import lifecycle events
- imports.svelte.ts: minor polish

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:33:31 +02:00
Till JS
5f0a1b5053 feat(articles): bulk-import UI (Phase 5)
apps/mana/apps/web/src/lib/modules/articles/components/:
  - BulkImportForm.svelte: <textarea> + live-validating $derived parser,
    counter chips for valid/duplicate/invalid, expandable invalid-list,
    submit creates a job + navigates to /articles/import/[jobId].
  - JobsList.svelte: index of past + active jobs (newest first), status
    pill + progress + per-counter chips. Click row → detail.
  - JobDetailView.svelte: live header (status, progress bar, counters),
    action bar (pause/resume/cancel/retry-failed/delete), per-item rows
    with state pill + URL + open-link or error tooltip.

apps/mana/apps/web/src/routes/(app)/articles/import/:
  - +page.svelte: hosts BulkImportForm + JobsList.
  - [jobId]/+page.svelte: hosts JobDetailView.

AddUrlForm.svelte: small "Mehrere URLs auf einmal? → Bulk-Import" link
under the single-URL input so the existing flow surfaces the new path.

The whole UI is a pure liveQuery view — JobDetailView re-renders as
the server-worker writes counter updates and item-state transitions
through sync_changes. Worker tick + pickup-consumer (already shipped
in 5535f2da4 + a9bcd4183) close the loop end-to-end.

Phase 6 (Domain-Events + AI-Tool) and Phase 7 (Tests) follow.

Plan: docs/plans/articles-bulk-import.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:30:36 +02:00
Till JS
29cbaf30f5 feat(articles): bulk-import store + queries (Phase 4)
apps/mana/apps/web/src/lib/modules/articles/:
  - stores/imports.svelte.ts: new file. articleImportsStore with
    createJob (bulkAdd N items + 1 job), pauseJob, resumeJob,
    cancelJob, retryFailed, deleteJob. parseUrls exported as a pure
    function — splits on whitespace+comma, validates http(s) scheme,
    deduplicates while preserving input order; used by both the store
    and the UI's $derived live-validation in Phase 5.
  - queries.ts: toImportJob/toImportItem converters + useImportJobs
    (index list), useImportJob (detail header), useImportItems (per-
    job item list). All scope-aware via scopedForModule / scopedGet.

Job creation: createJob(urls) → jobId. Items written first so a worker
tick that races the job-write doesn't see a job with totalUrls=N but
fewer items reachable. Server-worker picks up state='pending' items
on its 2s tick.

retryFailed re-arms the job to status='running' if it was 'done',
because all-terminal-items had triggered the auto-completion in the
worker's counter-rollup pass.

deleteJob is soft (deletedAt stamp) on both job + items; already-
landed Article rows are NOT touched.

Phase 5 (UI) follows.

Plan: docs/plans/articles-bulk-import.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:23:45 +02:00
Till JS
fa299e3bf9 feat(app-registry): wire up 4 modules + 7 routes + tier-patch validator
Resolves the cross-cutting drift that the app-registry sanity-test was
silently catching but BRANDING_ONLY exceptions papered over.

App-registry wiring:
- Register augur, broadcasts, invoices, timeline as workbench cards.
- Resolve agents↔ai-agents naming drift: workbench id is now `agents`
  (matches MANA_APPS + the /agents route URL); folder stays `ai-agents`
  for grouping with other ai-* modules.

Broadcast→broadcasts unification:
- module.config appId, MANA_APPS id, APP_ICONS key, all route appIds,
  and the redundant APP_URL_OVERRIDES entry — all aligned with the
  earlier folder rename so nothing diverges anymore.

Top-level routes for workbench-only modules:
- /goals, /myday, /kontext, /rituals, /automations, /activity — thin
  RoutePage wrappers around the existing module ListViews.
- /timeline becomes a real module (ListView extracted from the route),
  route shrinks to a 12-line wrapper.

Food unarchive:
- packages/shared-branding/src/mana-apps.ts: remove `archived: true`
  from food entry. The module is fully wired (registered, synced,
  routed, with AI tools); the flag was outdated.

i18n cleanup:
- Rename ai-agents → agents key in all 5 apps locales.
- Drop dead "observatory" key from all 5 nav locales (route folder was
  removed in 7bca16dfa).

New CI guard — scripts/validate-tier-patches.mjs:
- Scans for `LOCAL TIER PATCH — revert before release` markers.
- Default: informational list (does not fail).
- Strict mode (MANA_TIER_PATCH_STRICT=1) for release/RC pipeline.
- Wired into validate:all.

Spec update:
- registry.spec.ts WORKBENCH_ONLY/BRANDING_ONLY: documented Settings
  family + AI Studio surfaces + intentionally-internal modules so the
  drift guard fires only on real drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:21:41 +02:00
Till JS
a9bcd4183a feat(articles): client-side pickup consumer (Phase 3)
Watches `articleExtractPickup` via liveQuery. For each row the server-
worker drops:

  1. Look up the matching `articleImportItems` row. Stale → just clean
     the inbox.
  2. Dedupe race: if the URL has been single-saved meanwhile, point
     the import item at the existing article (state='duplicate'),
     don't create a second row.
  3. Happy path: call existing articlesStore.saveFromExtracted (which
     runs encryptRecord + articleTable.add and emits ArticleSaved)
     → flip item to 'saved' (or 'consent-wall' on warning).
  4. Delete the pickup row so the inbox stays empty in steady state.

Multi-tab coordination via `navigator.locks.request('mana:articles:pickup')`
with `ifAvailable: true` — only the lock-holder consumes; other tabs
just observe the liveQuery and exit. Falls back to per-row in-memory
dedupe when the Locks API isn't available; the field-LWW server merge
forgives the rare double-process.

Wired from data-layer-listeners.ts so it boots once with the rest of
the data layer and disposes on layout unmount.

End-to-end pipeline now live:
  Client write items(state='pending')
    → sync_changes
    → server-worker tick (Phase 2)
    → Pickup row + state='extracted'
    → sync pull → liveQuery
    → saveFromExtracted (encrypt) → flip 'saved' / 'duplicate' / 'consent-wall'
    → delete pickup row

What's still needed for first user-visible test: Phase 4 (store
methods to create a job) + Phase 5 (UI). Without those there's no
way yet to inject items.

Plan: docs/plans/articles-bulk-import.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:16:10 +02:00
Till JS
2bbcf14aba chore(geocoding): remove Pelias + close 3 bypass paths to public Nominatim
Pelias was retired from the Mac mini on 2026-04-28; photon-self
(self-hosted Photon on mana-gpu) has been the live primary since then.
This removes the now-dead Pelias adapter, config, tests, and the
services/mana-geocoding/pelias/ stack — the entire compose file, the
geojsonify_place_details.js patch, the setup.sh import script.

Provider chain is now `photon-self → photon → nominatim`. The chain
keeps its `privacy: 'local' | 'public'` split, sensitive-query
blocking, coord quantization, and aggressive caching unchanged.

Three direct calls to nominatim.openstreetmap.org that bypassed
mana-geocoding now route through the wrapper:

- citycorners/add-city + citycorners/cities/[slug]/add use the shared
  searchAddress() client (browser → same-origin proxy → mana-geocoding
  → photon-self).
- memoro mobile drops its OSM reverse-geocoding fallback entirely;
  Expo's on-device reverse-geocoding stays as the sole path. Routing
  through the wrapper would require a memoro-server proxy endpoint —
  a follow-up if Expo's quality proves insufficient.

Other behavioral changes:

- CACHE_PUBLIC_TTL_MS dropped from 7d → 1h. The long TTL was a
  privacy-amplification trick from the Pelias era; with photon-self
  serving the bulk of traffic, a transient cross-LAN blip was pinning
  cached fallback answers for days. 1h gives quick recovery.
- /health/pelias renamed to /health/photon-self; prometheus blackbox
  config + status-page generator updated.
- mana-geocoding container no longer needs `extra_hosts:
  host.docker.internal:host-gateway` (was only there for the
  Pelias-on-host-network era).

113 tests passing. CLAUDE.md rewritten to reflect the post-Pelias
architecture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:12:26 +02:00
Till JS
7bca16dfa7 feat(articles): bulk-import schema + plan (Phase 1)
Three new sync-tracked Dexie tables under the articles appId:

  articleImportJobs     — job header (counters, status, lease metadata).
  articleImportItems    — one row per URL in a job, state-machine driven.
  articleExtractPickup  — short-lived server→client handoff inbox.

URL stays plaintext on items by necessity — the server-worker reads it
without master-key access, same rationale as articles.originalUrl. The
extracted article eventually lands encrypted in the existing `articles`
table; bulk-import rows hold only pointers.

Plan: docs/plans/articles-bulk-import.md (full architecture, 7 phases,
test matrix, edge-cases). Phase 2 already shipped in 5535f2da4 (worker);
this commit lays the schema underneath it.

Originally committed as b2f4e8314, lost during a parallel reset, here
restored via cherry-pick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:11:51 +02:00
Till JS
248549b15a fix(feedback): keine doppelte Anzeige von Title + Body
Bei kurzen Posts (oder wenn mana-llm fehlschlug) hat der Auto-Title-
Fallback `feedbackText.slice(0, 80)` den Body 1:1 als Title gespeichert
— Card zeigte dann zwei Mal denselben Text.

Zwei Schichten Schutz:

1. **Server (mana-analytics)**: catch-Branch wirft den Prefix-Fallback
   raus (title bleibt null). Zusätzlich neue isRedundantTitle()-Heuristik
   verwirft auch Auto-Titles, die nur ein truncierter Prefix des Bodies
   sind (Whitespace-collapse + Ellipsis-strip).

2. **Frontend (ItemCard)**: defensive showTitle-Computed — ältere DB-
   Items mit redundantem Title rendern automatisch nur den Body, ohne
   dass eine Datenbank-Cleanup nötig ist.

Title-Slot bleibt für echte Auto-Summaries und manuelle Titel sichtbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:37:51 +02:00
Till JS
f851f15a47 feat(lasts): tidy ListView header — single-row quick-add + scrollable icon-tabs
Two layout fixes for the Lasts ListView:

1. Tab bar: status filters (Alle/Vermutet/Bestätigt/Aufgehoben) get inline
   Phosphor icons + parenthesized counters. Inbox/Meilensteine/Einstellungen
   now render as full icon+label tabs in a `border-left`-separated cluster
   instead of icon-only links. The whole bar is `overflow-x: auto` with
   hidden scrollbars (matches calendar/DateStrip pattern), so narrow
   workbench cards scroll horizontally instead of wrapping.

2. Quick-add: collapses two rows (input + Vermutet/Bestätigt pill toggle)
   into one. Mode is a `<select>` styled like the category select, sitting
   to the right of the title input. Removes the visual duplication where
   the toggle pills mimicked the status tabs above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:22:40 +02:00
Till JS
941df57f77 feat(feedback): rename community-identity columns + settings-section
Letzter "community"-Rest aus dem Feedback-Hub räumt sich auf — DB-Spalten,
Settings-Search-Index, Section-Name und i18n-Keys einheitlich auf
"feedback":

- DB: auth.users.community_show_real_name → feedback_show_real_name,
  community_karma → feedback_karma. Migration unter
  services/mana-auth/sql/009_rename_community_to_feedback.sql (manuell
  via psql, in Drizzle-Schema beider Services nachgezogen).
- mana-auth/me.ts: PATCH /api/v1/me/profile akzeptiert jetzt
  feedbackShowRealName und gibt es im Response zurück.
- mana-analytics: feedback.ts liest authUsers.feedbackShowRealName /
  feedbackKarma, redact() + Karma-Increment + Tests entsprechend.
- Frontend: CommunitySection.svelte → FeedbackIdentitySection.svelte
  (Datei umbenannt, Property-Namen + Toast-Texte aktualisiert,
  HeartHalf-Icon, "Feedback-Identität" als Title).
- searchIndex.ts: CategoryId 'community' → 'feedback', anchor
  'community-identity' → 'feedback-identity'.
- i18n (5 locales): settings.categories.community → .feedback,
  settings.search.community_* → feedback_*. Labels DE/EN/FR/IT/ES
  jeweils auf "Feedback" + "im Feedback-Feed" angepasst.

38/38 Integration-Tests grün, validate:i18n-parity sauber, svelte-check 0.

BREAKING (intern, nicht live): Frontend, das gegen die alten Spalten- /
Property-Namen aus dem PATCH-Response geht, fällt jetzt um. Kein
Production-Risiko da Hub noch nicht öffentlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:09:58 +02:00
Till JS
6f83fba66a docs(reports): geocoding self-hosting decision — recommend Photon on mana-gpu
Compares Pelias / Nominatim / Photon for self-hosting on the GPU
server, with current (2026-04-28) numbers from upstream docs +
GraphHopper's Photon-data downloads:

  Photon Europe pre-built dump: 30.6 GB, weekly refresh
  Photon Germany pre-built dump: 5.8 GB, weekly refresh
  Nominatim Germany import:     ~100 GB disk, 8–12 h, 12 GB RAM
  Pelias DACH (current):         3 GB RAM, 4 services, JS patch hack

Recommendation: Photon Europe-wide on mana-gpu. Single Java process,
embedded OpenSearch, no PBF import (download a tarball, restart),
weekly auto-updates from GraphHopper, integrates with the wrapper's
existing PhotonProvider via just an env-var change.

Once self-hosted, Photon registers as `privacy: 'local'` — the
sensitive-query block (Hausarzt, Klinikum, …) gets a real local
backend and no longer has to return empty results when Pelias is
down. Public Photon stays in the chain as a `privacy: 'public'`
last-resort fallback.

Migration plan included (~3–4 h total, ~1 h waiting), with
phase-by-phase risk assessment.

Pelias does not return — the 3 GB RAM + multi-container + patched
JS combination has no operational case once we have a self-hosted
Photon that already matches our wrapper's wire format.
2026-04-28 17:04:30 +02:00
Till JS
b1fa55dbca feat(places): surface geocoding privacy notices in autocomplete UI
The mana-geocoding wrapper now returns `notice: 'fallback_used' |
'sensitive_local_unavailable'` alongside results so the UI can show
the user *why* a query had unusual behavior. This commit wires that
all the way through the Places module's address-autocomplete inputs.

Geocoding client (lib/geocoding/index.ts):
- Add `GeocodingNotice` and `SearchOutcome` types
- Add `searchAddressDetailed` and `reverseGeocodeDetailed` — same
  semantics as the existing functions but return the wrapper's
  provider/notice metadata. Existing `searchAddress`/`reverseGeocode`
  stay backward-compatible (they call the detailed variants under
  the hood and discard the metadata).
- Extend GeocodingResult with optional `provider` field.

Places ListView (the only current consumer that exposes typed
addresses to users):
- Both autocomplete inputs (tracking-edit + main address-search)
  now use searchAddressDetailed and surface notices inline.
- 'sensitive_local_unavailable' renders an amber explainer block in
  the dropdown — title + body — so the user knows why their medical
  query returned 0 hits without leaking the search to a public API.
- 'fallback_used' renders a small "≈ ungefähr" footer badge so users
  understand the result came from public OSM (less precise but
  still valid).
- The dropdown opens when EITHER results exist OR a notice is
  present — sensitive blocked queries with empty results still
  surface their explainer.

i18n: new `places.geocoding_notice.*` sub-namespace in all 5 locales
(de/en/es/fr/it) — 4 strings each. All validators green.

Other consumers (places DetailView, events, photos, contacts) keep
the existing searchAddress/reverseGeocode calls — they don't need
the privacy notices today and would just add noise. They can adopt
the detailed variant if/when the use case warrants it.
2026-04-28 16:24:15 +02:00
Till JS
112e2cc1b4 feat(feedback): rename community → feedback (module + routes + domain)
Modul, Routen und Public-Domain heißen jetzt einheitlich "feedback":

- App-Registry: id 'community' → 'feedback', name 'Community' → 'Feedback',
  Icon Megaphone → HeartHalf (passt zum bereits-globalen heart-half-Icon
  am Module-Header und im PillNav-Usermenü)
- Modul-Config: communityModuleConfig → feedbackModuleConfig
- Routen-Refs: alle href/goto-Aufrufe in Modul-Views, MyWishesView,
  Onboarding-Wish, Profile-MyWishes auf /feedback umgestellt
- /feedback/+layout: Brand "Mana Community" → "Mana Feedback", Megaphone
  → HeartHalf, "In Mana öffnen"-CTA zeigt jetzt auf /?app=feedback
- Public-Mirror Domain: community.mana.how → feedback.mana.how
  (cloudflared-config.yml + docker-compose.macmini.yml CORS_ORIGINS +
  PUBLIC_MANA_ANALYTICS_URL_CLIENT). DNS muss separat angelegt werden.
- Settings-Section: Hilfe-Text nennt jetzt feedback.mana.how

Internal: community_show_real_name + community_karma DB-Spalten bleiben
(Migration nicht im Scope dieses Renames). Settings-Search-Index-Kategorie
'community' bleibt ebenfalls — sie spiegelt das DB-Schema, nicht den
User-Begriff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:18:45 +02:00
Till JS
bcc21ca785 feat(geocoding): privacy hardening — sensitive-query block + coord
quantization + extended cache TTL for public answers

Three independent defenses limit what public geocoding APIs (Photon,
Nominatim) can learn from our outbound traffic:

1. **Sensitive-query block** (`lib/sensitive-query.ts`)
   Queries matching the medical/mental-health/crisis-service keyword
   list (Hausarzt, Psychiater, Klinikum, HIV, Frauenhaus, …) are
   never forwarded to public APIs. The chain detects sensitivity at
   the route layer and runs the search in localOnly mode — providers
   with `privacy: 'public'` are filtered out before iteration begins.
   When no local provider is available (Pelias stopped), a sensitive
   query returns ok:true with results:[] and notice:
   'sensitive_local_unavailable' so the UI can show a sensible
   message instead of "no results".

   The keyword list is documented inline. False negatives are the
   risk; false positives just produce a 0-result UX hit (better
   trade-off).

2. **Coordinate quantization** (`lib/privacy.ts`)
   Forward-search focus.lat/lon: rounded to 2 decimals (~1.1km).
     Enough for the bias to work, hides exact GPS.
   Reverse-geocoding lat/lon: rounded to 3 decimals (~110m).
     City-block resolution — sufficient for "what's near me?",
     avoids reverse-geocoding the user's exact front door.
   Pelias always gets full precision; quantization only on the way
   out to public APIs. New `privacy: 'local' | 'public'` field on
   the GeocodingProvider interface drives this.

3. **Extended cache TTL for public answers**
   New `cache.publicTtlMs` config option, default 7 days (vs. 24h
   for local-provider answers). LRU cache extended with optional
   `ttlOverrideMs` per entry. Same query from N users → 1 outbound
   request to Photon/Nominatim. Strongest privacy lever we have
   over public providers (we can't change their logging, only the
   rate at which we feed them queries).

Threat coverage:
   ✓ User IP / identity hidden (already true — wrapper is the proxy)
   ✓ Exact GPS hidden (quantization)
   ✓ Sensitive query content protected (block)
   ~ Non-sensitive query content visible (acceptable trade-off)
   ~ Aggregate profiling reduced ~10–100× (cache)
   ✗ TLS-level traffic analysis, compelled disclosure (out of scope)

Tests: 141 (was 115). New coverage:
- privacy.test.ts: quantization rules (locks the privacy claim)
- sensitive-query.test.ts: positive matches across categories +
  documented false positives we accept
- chain.test.ts: localOnly mode end-to-end including the load-
  bearing assertion that public providers' search() must NEVER be
  called when the chain is in localOnly mode (no race window)
- cache.test.ts: per-entry ttlOverride longer + shorter than default

Live smoke verified end-to-end:
- "Hausarzt Konstanz" with Pelias down → no public API call,
  notice: 'sensitive_local_unavailable'
- "Konstanz" → falls through to Photon, notice: 'fallback_used'
- Reverse with high-precision GPS → Photon receives quantized
  coords, returns city-block-level result
2026-04-28 16:04:56 +02:00
Till JS
15ab24bda8 feat(feedback): heart-half als globales Feedback-Icon + inline-Form in der Workbench
Drei Probleme adressiert:

1. **Icon-Vereinheitlichung**: alle Feedback-Affordances tragen jetzt
   das phosphor `heart-half`-Icon (statt vorher Lightbulb/Mix). Geändert
   in PillNav-Usermenü, ModuleShell-Header (FeedbackHook), Phosphor-Icon-
   Map. Eine Stelle, ein Icon — Wiedererkennung steigt.

2. **Inline statt Modal in Workbench-Cards**: AppPage.svelte rendert
   das Feedback-Formular jetzt im selben Slot wie die Hilfe-Seite —
   Klick auf das Heart-Half-Icon togglet den Inline-Panel statt einen
   Modal-Backdrop über die ganze Workbench zu legen. Hilfe und Feedback
   sind mutually-exclusive (eines geht zu, sobald das andere aufgeht).

3. **Form-Body extrahiert**: FeedbackForm.svelte enthält jetzt das
   Formular ohne jegliches Chrome. FeedbackQuickModal nutzt es im Modal-
   Mode (Standalone-Routen, PillNav), AppPage im Inline-Mode. Eine
   Quelle, beide Surfaces bleiben in sync.

ModuleShell schluckt zusätzlich `onFeedback`/`feedbackOpen`-Props: wenn
gesetzt, ruft die FeedbackHook-Komponente onClick statt das eigene Modal
zu öffnen — der Host (AppPage) übernimmt das Rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:36:52 +02:00
Till JS
94d3277e2e feat(feedback): "Idee teilen" lebt jetzt im PillNav-Usermenü
Ersetzt den schwebenden "Idee?"-Pill durch einen Eintrag im rechten
Usermenü (Profil / Credits / Idee teilen / Logout). Ein Affordance an
einer Stelle statt zwei nebeneinander.

- PillNavigation: neuer onFeedback-Prop + Lightbulb-Icon. Wenn gesetzt,
  ersetzt der Eintrag den Legacy-/feedback-Link in accountLinks und
  taucht zusätzlich oben in den userMenuBarItems (barMode) auf.
- UserMenuPanel: AccountLink kennt jetzt onClick? als Alternative zu
  href? — Action-Chips schließen das Panel direkt nach dem Klick.
- (app)/+layout: GlobalFeedbackPill-Mount entfernt, FeedbackQuickModal
  wird state-gebunden gerendert (moduleContext aus Pfad/?app= abgeleitet
  wie bisher in der alten Pill).
- GlobalFeedbackPill.svelte gelöscht — niemand referenziert sie mehr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:12:27 +02:00
Till JS
eaa1d7432b fix: silence two cosmetic boot-time devtools warnings
1. /api/auth/organization/get-active-member 400
   The Better-Auth org plugin returns 400 ("active organization not
   found") whenever the session has no activeOrganizationId yet — i.e.
   on every fresh inkognito login. The fetch was already tolerated
   (fetchActiveMember returns null on 400), but the network panel
   logged it as a noisy red row.

   Fix: gate the call on the localStorage hint. The hint is set by
   writeActiveSpaceHint() after every successful set-active, so its
   presence is a reliable proxy for "session has activeOrganizationId
   set". Without a hint we go straight to list + auto-activate
   Personal — same effective outcome, no 400.

2. Chrome "Autofocus processing was blocked" on /onboarding/name and
   /onboarding/wish
   The static `autofocus` attribute races the previous route's focus
   owner across the SvelteKit transition. Chrome refuses to honour
   autofocus when a document already has a focused element and warns.

   Fix: replace the attribute with `bind:this={el}` + a $effect that
   imperatively `el.focus()`s after `tick()` — by then the outgoing
   page has unmounted and there's no competing focus claim. The
   svelte-ignore directives are no longer needed and have been removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:10:15 +02:00
Till JS
0c30a16eb5 fix: 4 boot-time noise + correctness bugs surfaced by post-deploy smoke
All four were pre-existing; the audit smoke-test made them visible. Fixed
together because they share a "boot console-warn cleanup" theme.

1. streaks ensureSeeded race (DexieError2 ×2)
   - Two boot-time liveQuery callers passed the `count > 0` check before
     either had written, then the second's `.add()` hit a ConstraintError.
   - Fix: cache the seed promise per module, run the existence check +
     bulkAdd inside one Dexie RW transaction, and only insert MISSING
     defs (preserves existing currentStreak/longestStreak counts).

2. encryptRecord('agents', …) "wrong table name?" warning
   - The DEV-only check fired whenever a record carried none of the
     registered encrypted fields, regardless of whether anything could
     actually leak. `ensureDefaultAgent` writes a fresh agent row before
     `systemPrompt` / `memory` exist — pure noise.
   - Fix: drop the "no fields at all" branch. Keep the case-mismatch
     branch (the branch that actually catches silent plaintext leaks).

3. Passkey signInWithPasskey "Cannot read properties of undefined
   (reading 'allowCredentials')"
   - Client destructured `{ options, challengeId }` from the server's
     options response, but Better-Auth's `@better-auth/passkey` plugin
     returns the raw PublicKeyCredentialRequestOptionsJSON (no
     envelope) and tracks the challenge in a signed cookie. Both
     `options` and `challengeId` came back undefined; SimpleWebAuthn
     blew up the moment it tried to read the request shape. Verify body
     `{ challengeId, credential }` was likewise wrong — Better-Auth
     wants `{ response }`.
   - Fix: align both register and authenticate flows with Better-Auth's
     native shape on options + verify, and add `credentials: 'include'`
     on every fetch so the challenge cookie actually round-trips.
     Server's verify proxy now reads `parsed?.response?.id` for
     credentialID rate-limiting.

4. /api/v1/me/onboarding/ → 404
   - Hono's nested router (`app.route(prefix, sub)` + inner
     `app.get('/')`) matches the prefix-without-slash form only. The
     onboarding-status store sent the request with a trailing slash, so
     every login produced a 404 + a console warn.
   - Fix: client sends the path without trailing slash; mana-auth picks
     up `hono/trailing-slash` middleware as defense-in-depth so a future
     accidental trailing slash on any /me/* route 301-redirects instead
     of 404-ing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:56:24 +02:00
Till JS
4237d84c18 i18n(drink+habits+picture): translate 3 list views via $_()
- drink/ListView: route through drink.list_view.* (today/log/empty
  + create form + ctx-menu); drops unused PencilSimple equivalent
- habits/ListView: route through habits.list_view.* (voice
  capture + tally grid + create form + ctx-menu); drops unused
  PencilSimple icon import
- picture/ListView: route through picture.list_view.* (drop overlay,
  action strip, view-mode titles, search placeholder, empty states,
  lightbox actions)

Baseline 833 → 818 (-15).
2026-04-27 22:36:57 +02:00
Till JS
0f1dbe9d4c i18n(locales): add drink+habits, extend picture for list-view sub 2026-04-27 22:32:59 +02:00
Till JS
7dfa1c74be i18n(body+mood+questions): translate picker/quick-log/question-detail via $_()
- body/components/ExercisePicker: route remaining German strings
  (dialog/close/filter_all/create-form) through body.exercisePicker.*;
  drops { default: '...' } fallbacks now that all keys resolve
- mood/components/QuickLog: route through mood.quick_log.*
- questions/views/DetailView: route through questions.detail.*
  + dynamic questions.status.<id> / questions.priority.<id> /
  questions.detail.depth_<id>; drops local statusLabels/priorityLabels/
  depthLabels const blocks (re-uses existing status+priority keys
  extended with `normal`/`urgent`)

Baseline 851 → 833 (-18).
2026-04-27 19:13:18 +02:00
Till JS
136d3fbf87 i18n(locales): extend body+mood+questions for picker/quicklog/question-detail 2026-04-27 19:11:02 +02:00
Till JS
a842537191 i18n(comic+quiz): translate picker/character-detail/play-view via $_()
- comic/components/CharacterPicker: route through comic.picker.* with
  HTML interpolation for the no-face/empty-garment alerts
- comic/views/DetailCharacterView: route through comic.character_detail.*
  + dynamic comic.styles.<id>; drops unused STYLE_LABELS import
- quiz/PlayView: route through quiz.play_view.* (back/empty/result/play
  all consolidated)

Baseline 869 → 851 (-18).
2026-04-27 18:47:37 +02:00
Till JS
5d9dc80662 i18n(locales): extend comic with picker+character_detail, quiz with play_view 2026-04-27 18:44:23 +02:00
Till JS
a5cef980ae i18n(music+profile): translate detail/hub views via $_()
- music/views/DetailView: route through music.detail.* (new namespace)
- profile/ListView: route through profile.hub.* (new sub-namespace)
  + reuse existing profile.* keys for account-section actions; TABS
  refactored from literal label → labelKey routing through $_()

Baseline 881 → 869 (-12).
2026-04-27 18:41:43 +02:00
Till JS
fa401cfeec i18n(locales): add music namespace + extend profile with hub sub 2026-04-27 18:39:50 +02:00
Till JS
b99dd60ad0 i18n(cards+finance+mood): translate 3 list/detail views via $_()
- cards/views/DetailView: route through cards.detail.* (deck-name
  fallback, prop labels, meta, confirm/toast strings)
- finance/ListView: route through finance.page.* (re-uses existing
  page namespace) + finance.list_view.empty_no_tx; drops unused
  Transaction + FinanceCategory type imports
- mood/ListView: route through mood.list_view.* (new namespace)

Baseline 899 → 881 (-18).
2026-04-27 18:38:06 +02:00
Till JS
70a06d1d9f i18n(locales): extend cards/finance + add mood namespace 2026-04-27 18:35:34 +02:00
Till JS
7339fba3aa i18n(inventory+questions+invitations): translate 3 routes via $_()
- inventory/items/[id]/+page: route through inventory.detail.* +
  dynamic inventory.status.<id>; drops local statusLabels constant
  (re-uses existing inventory.status.* keys). Fixes pre-existing
  typos endgultig/loschen/Zuruck/hinzufugen via proper translations.
- questions/+page: route through questions.home.* + dynamic
  questions.home.depth_<id>; locale-aware date formatting via
  get(locale) instead of hardcoded de-DE; drops unused
  ResearchDepth import + depthLabels const.
- accept-invitation/+page: route through invitations.accept.*
  (new namespace); space-type label still uses SPACE_TYPE_LABELS
  from shared-branding (only de/en available — locale-prefix gate).

Baseline 920 → 899 (-21).
2026-04-27 18:29:17 +02:00
Till JS
ef3243a68a i18n(locales): extend inventory + questions, add invitations namespace 2026-04-27 18:26:15 +02:00
Till JS
3abcbd4f4d i18n(wetter+profile+contacts): translate 3 detail/freeform/comparison views via $_()
- wetter/components/SourceComparison: route through wetter.comparison.*
  (also fixes pre-existing typos verfuegbar/Gefuehlt → verfügbar/gefühlt
  via proper translations across all 5 locales). Renamed unused #each
  param `_` → `_ignored` to avoid shadowing svelte-i18n's $_.
- profile/ContextFreeform: route through profile.freeform.*; injected
  markdown source label uses i18n key too
- contacts/[id]/+page: route through contacts.detail.*; replaces typoed
  "endgueltig loeschen"/"geloescht"/"Loeschen"/"Zurueck"/"E-Mail-Mobil"
  fallbacks with proper umlauted translations. Drop unused Observable
  import.

Baseline 940 → 920 (-20).
2026-04-27 18:23:29 +02:00
Till JS
c2660dd6b2 i18n(locales): add wetter + extend profile/contacts for next 3 detail/freeform/comparison views 2026-04-27 18:19:51 +02:00
Till JS
63b9ff4684 i18n(comic+guides+cards): translate 3 detail/progress views via $_()
- comic/views/DetailView: route through comic.detail.* + dynamic
  comic.styles.<id>; drop unused STYLE_LABELS import
- guides/views/DetailView: route through guides.detail.* + dynamic
  guides.categories.<id> / guides.difficulties.<id>; drop unused
  DIFFICULTY_LABELS + Section type
- cards/progress/+page: route through cards.progress.* (also fixes
  pre-existing typos "Fallig"/"Ubersicht"/"Lernsitzungen" via
  proper translations across all 5 locales)

Baseline 961 → 940 (-21).
2026-04-27 18:17:08 +02:00
Till JS
e3c2b26510 i18n(locales): add comic + extend cards/guides for next 3 detail/progress views 2026-04-27 18:14:14 +02:00
Till JS
98a9bc4dc5 i18n(agents/templates): translate /agents/templates +page.svelte via $_()
Adds agents.templates namespace covering page title + back link, header
sub copy, 2 section headings + descriptions, 4 chip variants (Agent/
Scene/Mission/Seeds with one/other plurals), detail no-agent role,
3 preview section headings (Scene-Layout/Starter-Missionen/Seeds), seed
hint + count plurals + unnamed fallback, 5 cadence variants (manual/
daily/weekly/interval/cron) with interpolation, options section + 4
checkbox labels, result strings (existing/new agent + scene + mission
active/paused + seed summary with/without failed), error fallback,
apply-button states (applying / "Template „{label}" anwenden").

Baselines: hardcoded 968 → 961 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:58:15 +02:00
Till JS
aa96cae8a0 i18n(wallpaper): translate WallpaperPicker via $_() — scope toggle, tabs, sections, upload, overlay
Adds wallpaper.picker namespace covering:
- Scope toggle (Alle Szenen / Nur diese Szene), Reset action
- 3 tabs (Farben/Bilder/Upload) routed via labelKey on the tab data
- Section labels (Empfohlen + Weitere)
- "Hintergrundbilder kommen bald" placeholder for empty images tab
- Upload zone (in-progress, drop, prompt, hint with formats)
- Upload error templates (failed with {status}, generic fallback)
- Loading gallery + "Eigene Bilder" section + delete-image title
- Overlay section + Weichzeichner/Abdunklung labels
- "Bild" alt fallback for media originalName

Baselines: hardcoded 975 → 968 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:08:37 +02:00
Till JS
4357433e7b i18n(contacts): translate /contacts +page.svelte via $_() — header, page picker, modal form
Adds contacts.page sub-namespace covering page title, header (title +
"{n} Kontakte" stats + Suchen placeholder + Neu action), 9 PAGE_META
labels (Mein Profil/Alle Kontakte/Favoriten/etc) — refactored to titleKey
routing through $_(), Modal title (edit vs new), 7 form section labels,
21 input placeholders (firstName/lastName/email/phone/company/etc),
Cancel/Save actions, "Seite hinzufügen" page-picker label.

Baselines: hardcoded 982 → 975 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:44:38 +02:00
Till JS
a5d4554c11 i18n(myday): translate ListView via $_() — 5 sections + alerts
Adds myday namespace covering 5 section headers (Tasks/Termine/Wasser/
Ernährung/Streaks), overdue alert with {n}, empty states (Keine Tasks
heute, Keine Termine), coffee+meals counters with {n} interpolation.
Misspelled "ueberfaellig"/"Ernaehrung" inputs corrected via Unicode
fallback in JSON.

Baselines: hardcoded 989 → 982 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:36:15 +02:00
Till JS
f92d6475f7 i18n(food/home): translate /food +page.svelte via $_() — header, progress cards, today's meals, links
Adds food.home sub-namespace covering page title, "Heute" heading + locale-
aware date subtitle, Verlauf/Mahlzeit actions, today's meals section, "{n}
Einträge" counter, empty state (no-meals + hint + add action), inline macro
labels ({n}g Protein/Carbs/Fett), Ziele/Verlauf footer links. Reuses
food.nutrition.* for the 4 progress-card labels.

Baselines: hardcoded 994 → 985 (7 cleared from food + 2 added by parallel
CommunitySection commit = net 9); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:34:25 +02:00
Till JS
1b30c36553 feat(settings): Community-Section mit Klarname-Toggle + Avatar/Karma-Preview
Settings → Community zeigt Pseudonym + Avatar + Tier-Badge + Karma,
plus Switch für 'Klarname neben Eule zeigen'. Optimistic-Update mit
Rollback bei Fehler. Suchindex + 5 Locales aktualisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:31:14 +02:00
Till JS
4ed8686ddc i18n(photos): translate FilterBar via $_() — App/Zeitraum/Sortierung/Reihenfolge + Reset/Apply
Reuses existing photos.filters.* (app, dateRange, date, size, sortOrder)
and adds 6 keys (sortByShort, createdAt, newestFirst, oldestFirst, reset,
apply) for the workbench-embedded filter bar's compact labels.

Baselines: hardcoded 1002 → 994 (8 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:26:51 +02:00
Till JS
de2134fb02 i18n(gifts/redeem): translate /[code] +page.svelte via $_() — info card, redeem flow, success state
Adds gifts.redeem sub-namespace covering page back-title + page-header,
err_not_found + err_redeem_failed fallbacks, toast_received with
{credits}, success state (heading/credits-label/balance-html/2 link
buttons), gift-not-found "Anderen Code eingeben", info card (Von {name},
Du erhältst, Credits, Art/Status/Gültig bis labels, Nachricht prefix),
section_redeem heading, 3 inactive warnings (depleted/expired/other),
personalized info, action_redeeming + action_redeem, getStatusLabel and
getTypeLabel switch cases routed through $_(). Date formatter switched
to get(locale) ?? 'de'.

Baselines: hardcoded 1009 → 1001 (8 cleared from gifts) + 1 added by
parallel community-eule commit = net 1002; missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:17:27 +02:00
Till JS
ee5bb2871c feat(community): Phase 3.C — Identität (Avatar + Klarname-Toggle + Karma + Eulen-Profil)
Macht aus den Pseudonymen echte Charaktere ohne Klarnamen-Zwang.

Pixel-Identicon-Avatar (3.C.2):
- generateAvatarSvg(displayHash) — pure-function, deterministisch.
  5×5 left-mirrored Identicon mit HSL-Foreground/Background aus dem
  Hash. Inline-SVG, kein Storage, kein img-load-Flicker.
- <EulenAvatar> Component im Package, in ItemCard neben dem Pseudonym.

Klarname-Toggle (3.C.1):
- auth.users + community_show_real_name boolean (default off, opt-in).
- PATCH /api/v1/me/profile akzeptiert communityShowRealName.
- mana-analytics LEFT JOINs auth.users → bei opt-in liefert auth-
  required /public + /me/reacted Endpoints zusätzlich realName.
- Anonymous /api/v1/public/feedback/* zeigt realName NIE — auch nicht
  wenn opted-in. Public-Mirror bleibt für SEO + Privacy safe.
- Migration 008_community_identity.sql lokal + prod eingespielt.

Karma-System (3.C.3):
- auth.users + community_karma int. toggleReaction increment/decrement
  am Author-User (Self-Reactions zählen nicht — kein Self-Farming).
- KARMA_THRESHOLDS + tierFromKarma() im Package: Bronze (0-9) /
  Silver (10-49) / Gold (50-199) / Platin (200+).
- ItemCard zeigt Tier-Dot neben dem Pseudonym, Title-Tooltip mit
  Karma-Zahl. Floor-clamped at 0.

Eulen-Profil (3.C.4):
- GET /api/v1/public/feedback/eule/{hash} — alle public-Posts dieser
  Eule + aggregiertes Karma. SHA256-Format-Validation.
- /community/eule/[hash] Public-SSR-Route mit Avatar-Hero, Tier-Badge,
  Karma-Counter, Post-Liste. Author-Klick im ItemCard navigiert hin.
- publicFeedbackService.getEulenProfile() im Package.

PublicFeedbackItem erweitert um displayHash (public Pseudonym-ID,
SHA256 ist one-way → safe to expose) + karma + optional realName.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:15:16 +02:00
Till JS
42e4d58c8c i18n(news/preferences): translate +page.svelte via $_() — header, all 5 sections
Reuses existing news.preferences sub-namespace (topicsHeading/Hint,
languagesHeading, sourcesHeading/Hint, weightsHeading/Hint/Reset,
weightsResetConfirm, onboardingHeading/Hint/Rerun) and adds 4 keys
(page_title_html, subtitle, sourcesHintHtml with {count}, sourcesLinkArrow).
Languages pills (Deutsch/English) routed via news.languages.*.

Baselines: hardcoded 1017 → 1009 (8 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:11:03 +02:00
Till JS
98ce33e788 i18n(memoro): translate views/DetailView via $_() — title sources, statuses, fields, transcript
- TITLE_SOURCE_LABELS map → TITLE_SOURCE_KEYS routing through $_(memoro.detail_view.title_sources.*)
- statusLabels map → STATUS_KEYS routing through $_(memoro.detail_view.statuses.*)
- Shell labels (notFound/confirmDelete/toast_deleted)
- Title placeholder: idle vs generating variant
- 4 prop rows (Status/Dauer/Sprache/Sichtbarkeit) + lang placeholder
- Section labels (Zusammenfassung/Transkript) + transcript states (transcribing/failed/empty/source)
- Meta-row Erstellt/Bearbeitet with {date}

Baselines: hardcoded 1025 → 1017 (8 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:07:35 +02:00
Till JS
d391a603f7 i18n(memoro): extend with detail_view sub-namespace
Adds detail_view: title_sources (5 LlmTier labels), statuses (4
ProcessingStatus labels), shell labels (notFound + confirmDelete +
toastDeleted), title placeholder + generating variant, 4 row labels
(Status/Dauer/Sprache/Sichtbarkeit) + lang placeholder, 2 section
labels (Zusammenfassung/Transkript) with placeholder + 4 transcript
states (transcribing/failed/empty/source), 2 meta keys with {date}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:05:49 +02:00
Till JS
092c45c835 i18n(places): translate views/DetailView via $_() — header, fields, sections, meta
- Shell labels (notFound + confirmDelete + Unbenannt fallback)
- Name input placeholder, map iframe title
- 5 row labels (Sichtbarkeit/Kategorie/Adresse/Koordinaten/Beschreibung) + Link share row
- Category options routed via $_('places.categories.' + v) — CATEGORIES constant inlined as PlaceCategory[] array
- Address + address-search placeholders, Lat/Lng coords placeholders, resolve title
- Tags / Letzte Besuche section labels
- 4 meta-row keys with {n}/{date} interpolation; toLocaleDateString switched to get(locale) ?? 'de'

Baselines: hardcoded 1033 → 1025 (8 cleared); missing-keys baseline +1 (places.categories.* dynamic key).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:02:54 +02:00
Till JS
53cf17a886 i18n(places): add namespace JSONs (de/en/es/fr/it)
Adds categories (7 PlaceCategory enum values) + detail_view sub-namespace
covering shell labels (notFound + confirmDelete + untitled fallback),
name placeholder, map title, 4 row labels (Sichtbarkeit/Kategorie/Adresse/
Koordinaten/Beschreibung), address-search placeholder, coords placeholders,
resolve title, tags + recent-visits sections, 4 meta-row keys with
{n}/{date} interpolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:00:51 +02:00
Till JS
66ab5f65f6 i18n(sleep): translate ListView via $_() — log CTA, last-night, week chart, stats, heatmap, hygiene
- Log CTA "Wie hast du geschlafen?" + "Jetzt loggen"
- Last-night card: "Letzte Nacht" label, "Bearbeiten" edit button, "{n}× aufgewacht" interpolation
- "Diese Woche" week section heading
- 5 stat labels (Ø Dauer (7T) / Ø Qualität / Schlafschuld / Konsistenz / Streak)
- Quality heatmap: section heading + cell title with {date}/{label} interpolation, label sourced via $_('sleep.qualities.' + n) (added qualities sub-namespace)
- Hygiene-correlation card: heading + with/without rows
- Action buttons: Schlaf loggen / Hygiene-Check
- QUALITY_LABELS import dropped (constant kept in types.ts for non-Svelte callers)

Baselines: hardcoded 1034 → 1033 (8 cleared from sleep + 7 added by parallel community-feature commits = net -1); missing-keys baseline +1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:58:45 +02:00
Till JS
3a18a5e50d feat(community): Phase 3.B — loop closure (notifications + my-wishes page)
Schließt den Loop zwischen Submit und Ship. User kriegt jetzt:
- Toast beim nächsten App-Start, wenn ein eigener oder unterstützter
  Wisch ›planned/in_progress/completed/declined‹ wurde
- /profile/my-wishes als persönliche Roadmap mit drei Tabs:
  Eigene · Unterstützt · Inbox

Server (mana-analytics):
- Neue Tabelle feedback_notifications mit ON DELETE CASCADE auf
  user_feedback. Migration 0004 lokal + prod eingespielt.
- adminUpdate enqueued bei jeder Status-Transition Author-
  Notifications. AdminResponse-Edits feuern eine eigene
  'admin_response'-Notify. tryGrantShipBonus hängt zusätzlich
  Reactioner-Notifications dran (›Dein Like ist gelandet, +25 Mana‹).
- Endpoints:
    GET  /api/v1/feedback/me/notifications?unread_only=true&limit=N
    POST /api/v1/feedback/me/notifications/:id/read
    POST /api/v1/feedback/me/notifications/read-all
    GET  /api/v1/feedback/me/reacted    (für die My-Wishes-Page)

Package (@mana/feedback):
- FeedbackNotification + NotificationKind types exportiert
- service.getNotifications/markNotificationRead/markAllNotificationsRead
- service.getMyReactedItems

Web:
- lib/notifications/feedback-toaster.svelte.ts: Boot-Pull + 60s-Poll,
  rendert unread-notifications via toast-store, markiert sofort read.
  In (app)/+layout.svelte's authReady-Hook gestartet/gestoppt.
- /profile/my-wishes: Tab-View über getMyFeedback + getMyReactedItems
  + getNotifications. Tabs zeigen Counter-Badges, unread-Badge in der
  Inbox-Sektion. ›Alle als gelesen markieren‹-Action vorhanden.

Pre-launch saubere Lösung — kein Polling-Spam (60s), Mark-Read direkt
nach Toast-Display, fail-soft an mehreren Stellen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:55:01 +02:00