Commit graph

3924 commits

Author SHA1 Message Date
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
164d5dab8b fix(mana-llm): copy aliases.yaml into Docker image
main.py's lifespan handler loads `Path(__file__).parent.parent /
'aliases.yaml'` (= /app/aliases.yaml) on startup. The Dockerfile only
copied `src/`, so prod containers always crashlooped on first start
with `AliasConfigError: alias config not found at /app/aliases.yaml`
— which is why mana-llm has been silently absent from prod. Surfaced
today after a manual `gh workflow run cd-macmini.yml -f service=
mana-llm` actually attempted to start the container instead of
relying on a long-stale image.

Tested locally: container now starts cleanly, /health returns 200,
and `/v1/aliases` lists the configured chains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:47:48 +02:00
Till JS
34b1690ea4 fix(mana-ai): copy missing workspace deps into Docker installer stage
The Dockerfile copied only @mana/shared-{hono,ai,logger}, but
services/mana-ai/package.json also depends on @mana/shared-research and
@mana/tool-registry (and @mana/tool-registry pulls in
@mana/shared-crypto transitively). Without those, pnpm couldn't
resolve the workspace symlinks and the container crashlooped on every
restart with:

  error: ENOENT reading "/app/services/mana-ai/node_modules/@mana/tool-registry"

Surfaced today after a manual `gh workflow run cd-macmini.yml -f
service=mana-ai` — mana-ai had never been deployed because no commit
since the CD pipeline started had touched its path. The first real
deploy hit the missing-COPY immediately.

Header comment in the Dockerfile now spells out direct + transitive
workspace deps so future additions don't drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:42:20 +02:00
Till JS
9a0cf5b676 fix(geocoding): bump PROVIDER_TIMEOUT_MS default 5s → 8s
First-probe DNS+TLS handshake against Nominatim can take >5s on a
cold start (verified locally: 642ms warm, sometimes 5-8s cold). The
old 5s default false-marked Nominatim unhealthy and the 30s health-
cache then locked us into a fallback-of-fallback gap. 8s gives
enough headroom for cold-start while still cutting off actually-
stuck connections.

Photon and Pelias don't hit this — Photon's CDN is consistently
sub-second and Pelias is on localhost / LAN. Only the public
Nominatim path warranted the bump, but the timeout is per-provider
shared so we adjust it globally.

Existing PROVIDER_TIMEOUT_MS env override still wins.
2026-04-28 15:39:09 +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
f39e72340c chore(ci): drop 16 dead build-* jobs + per-product detect-changes branches
The CI workflow accumulated 16 build jobs for apps/services that no
longer exist after the 2026-04 unification (chat, todo, calendar, clock,
contacts, presi, storage, food, skilltree, telegram-stats-bot — all of
their /apps/web and /apps/backend dirs are gone) plus a structurally
broken `build-mana-web` (its Dockerfile starts FROM `sveltekit-base:local`,
an image only the Mac Mini self-hosted runner has). Every push has been
producing red CI runs from these dead jobs while the real production
deploys (cd-macmini.yml) succeeded.

Removed:
- detect-changes per-product outputs + force-build-all branches
- 220 lines of dead per-product detect-logic shell
- 19 lines of per-product summary block
- build-mana-web (broken; CD on Mac Mini covers prod)
- build-{chat,todo,calendar,clock,contacts,presi,storage,food,skilltree}-{web,backend}
- build-telegram-stats-bot

Kept (still build cleanly on ubuntu-latest):
- build-mana-{auth,search,sync,notify,api-gateway,crawler,media,credits}
- validate (PRs)
- auth-integration (PRs)

CI workflow shrank 1290 → 583 lines. The header comment now spells out
which services are in/out of CI and why.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:32:43 +02:00
Till JS
f1e4a39644 feat(geocoding): provider chain with Photon + Nominatim fallbacks
mana-geocoding now tries Pelias first, falls back to public Photon
(komoot.io) and finally to public Nominatim (OSM) when Pelias is
unhealthy or unreachable. The Places module's address lookup keeps
working even when the Pelias container is stopped — which it currently
is on the Mac mini, freeing 3 GB of RAM until Pelias gets migrated to
the GPU server.

Architecture:

  ProviderChain ─ tries providers in priority order, stops on first
                  success. A clean empty-results answer is definitive
                  (don't burn through public-API budget on a query that
                  legitimately has no match). Only network errors / 5xx
                  / 429 trigger fallthrough.

  HealthCache  ─ per-provider, 30s TTL. A failed health probe or a
                  failed search marks the provider unhealthy and skips
                  it for the rest of the cache window. Lazy refresh —
                  no background pinger.

  RateLimiter  ─ single-token FIFO queue, 1100ms gap by default.
                  Used to enforce Nominatim's 1 req/sec policy. Handles
                  abort during inter-task wait by releasing the busy
                  flag so later tasks aren't blocked.

Provider details:

  pelias    — primary, self-hosted DACH index, full OSM taxonomy in
              `peliasCategories`, no rate limit
  photon    — public komoot endpoint, GeoJSON shape, raw `osm_key:
              osm_value` mapped via lib/osm-category-map.ts. Faster
              than Nominatim, no advertised rate limit but be polite.
  nominatim — public OSM endpoint, strict 1 req/sec via the limiter,
              custom User-Agent required (otherwise 403). Last
              resort — fallback for when Photon is also down.

Response shape changes (additive only — existing callers keep
working):

  - results[].provider: 'pelias' | 'photon' | 'nominatim'
  - results[].peliasCategories: only present when Pelias served the
    request (was already absent on Pelias-API patch failures)
  - top-level provider: <name> + tried: <name[]> on success/error
  - new endpoint: GET /health/providers — per-provider snapshot

Configuration via env (defaults shipped):

  GEOCODING_PROVIDERS=pelias,photon,nominatim   # order matters
  PROVIDER_TIMEOUT_MS=5000
  PROVIDER_HEALTH_CACHE_MS=30000
  PHOTON_API_URL=https://photon.komoot.io
  NOMINATIM_API_URL=https://nominatim.openstreetmap.org
  NOMINATIM_USER_AGENT=mana-geocoding/1.0 (+https://mana.how; ...)
  NOMINATIM_INTERVAL_MS=1100

Testing: 115 tests green (was 42). New coverage:
  - osm-category-map.test.ts (47 cases over food/transit/shopping/
    leisure/work/other priority resolution)
  - rate-limiter.test.ts (FIFO, abort-during-wait, abort-during-sleep)
  - chain.test.ts (failover, empty-results-stops, health-cache,
    snapshot)
  - photon-normalizer.test.ts and nominatim-normalizer.test.ts (lock
    the wire-format mapping for both fallback providers)

Live smoke against public Photon verified — both /search and /reverse
return correctly normalized results with provider="photon" when Pelias
is unreachable.
2026-04-28 15:21:11 +02:00
Till JS
ff823bff60 fix(feedback): POST /api/v1/feedback liest appId aus X-App-Id-Header
Der Submit-Handler hat den Body 1:1 an feedbackService.createFeedback
weitergereicht. Da CreateFeedbackInput appId nicht enthält (Client
schickt es als X-App-Id-Header), schlug jeder INSERT mit "null value
in column app_id violates not-null constraint" fehl.

Außerdem: lightbulb-Icon im phosphor-icon-map nachgezogen, sonst
zeigt der "Idee teilen"-Eintrag in der barMode-Variante des Usermenüs
kein Icon (nur Label).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:16:11 +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
537719032e infra(macmini): bump squeezed container memory limits
Mac Mini was running at 99% memory pressure with 8.6 GB swap active —
load was OK but every cold-container request was paying disk-I/O for
swapped pages. Container observations:

  redis      190/192 MB (99 %)  — close to OOM, hot keys evicting
  victoria   227/256 MB (89 %)  — constant GC pressure
  glitchtip  232/256 MB (91 %)
  umami      223/256 MB (87 %)

Each bumped to 384 MB, total +512 MB reservation in the Colima VM.
Headroom for that comes from stopping the Pelias stack (~3 GB freed)
in the same change-window.

Redis additionally gets `--maxmemory 320mb --maxmemory-policy allkeys-lru`
so the daemon evicts its own LRU keys at ~80 % of mem_limit instead of
letting the kernel OOM-kill the whole container. Safe for our usage —
Redis only holds rate-limit counters + sync hot-paths, no critical state.

Pelias stays stopped pending a migration to mana-gpu; mana-geocoding
will need a Nominatim fallback before the migration so the Places
module's address lookup keeps working.
2026-04-28 15:02:38 +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
44f9155ed3 chore(dev): pnpm dev:analytics script + test-checklist mentions local-dev startup
War nicht im Setup dokumentiert: bei localem Web-Dev (5173) muss
mana-analytics auf 3064 laufen, sonst werfen FeedbackHook + Toast-
Poll + /community ein ERR_CONNECTION_REFUSED. Convenience-Script
+ Hinweis in der Test-Checklist verhindern den Stolperer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:54:32 +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
0986d07a7d docs: feedback-hub manual-test-checklist
15 sections covering Phase 3 end-to-end browser-test-flow:
Onboarding-Wish, FeedbackHook + Modal, Public-Feed (eingeloggt +
inkognito), Reactions + Karma, Status-Flow + Loop-Closure, AdminResponse,
Klarname-Toggle, Eulen-Profil (SSR + 404), Threading, Phase-3.F-Cleanup-
Verifikation, Founder-Whitelist, Rate-Limit, Voting-Score, Mobile-
Responsiveness, Quick-DB-Sanity-SQLs.

Plus 'wenn was kaputt ist'-Debug-Pfad und bekannte Lücken (Email-Notifs,
Voice-Submit, Trending, Karma-Decay) als Roadmap-Markers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:33:28 +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
e773e44cdf test(feedback): DB-backed integration tests — credits, karma, notifications
22 neue Integration-Tests gegen real Postgres, plus auth.users-Cross-
Schema-Joins, plus mock-fetch für mana-credits-Calls. Skip-pattern via
TEST_DATABASE_URL — fresh checkout's bun test passes without a DB.

src/test-helpers/db.ts:
- connectTestDb: drizzle + postgres-js mit voller schema-typing
- seedUser: namespaced 'test-${uuid}' userIds, raw-SQL insert in
  auth.users (cross-schema, kennt mana-analytics nicht alle NOT-NULL
  columns)
- cleanupTestData: FK-aware DELETE chain (notifications → reactions
  → grant_log → feedback → users)
- mockCreditsFetch: globalThis.fetch-shim, captures grant-calls,
  configurable alreadyGranted für idempotency tests

src/services/feedback-credits.integration.test.ts (11 tests):
- submit-bonus: 20+ chars happy path, <20 chars skip, replies skip,
  founder-whitelist skip, rate-limit (11. submit blocked),
  alreadyGranted doesn't write log row
- ship-bonus: +500/+25-emoji-eligible matrix, status-flapping doesn't
  double-pay (idempotent referenceId), founder-author skip
- reaction-bonus: self-react skip, multi-emoji-same-user counts once

src/services/feedback-karma.integration.test.ts (5 tests):
- foreign user reacts → author karma +1
- toggle off → karma -1
- self-react → no change
- floor-clamped at 0
- multi-emoji per user counts separately

src/services/feedback-notifications.integration.test.ts (6 tests):
- author-notify on every status transition
- no notify when status unchanged
- admin_response notify on adminResponse edit
- reactioner_bonus notify on completed transition (+25 in body)
- markRead scopes to caller (stranger can't mark)
- markAllRead only touches caller's rows

Plus:
- package.json: test + test:integration scripts
- schema/index.ts: re-export auth-users so drizzle's type inference
  recognizes the cross-schema model
- CLAUDE.md: full rewrite — endpoints (auth + public), env vars,
  test commands, reward-loop side-effects table

All 38 tests green (16 unit + 22 integration) in 2.35s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:08:29 +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
246c94374f test(feedback): pixel-avatar + redact privacy-boundary; mark plan SHIPPED
Tests:
- packages/feedback/src/avatar.test.ts — 10 unit tests (determinism,
  mirror-symmetry, color contrast, padding-resilience, pseudonym-
  integration, density-sanity).
- services/mana-analytics/src/services/feedback-redact.test.ts —
  9 privacy-boundary tests verifying:
    * anonymous path NEVER includes realName, even when author opted in
    * auth path NEVER includes realName when author opted OUT
    * realName only when (opted-in AND auth-path) — both gates required
    * userId / deviceInfo / voteCount stripped from output

Plan-Doc:
- docs/plans/feedback-rewards-and-identity.md status → shipped (3.A,
  3.B, 3.C, 3.F live; 3.D, 3.E open) mit Commit-Hashes.

Service-Layer minor: REWARD-const + redact als __TEST__-Export
publik gemacht (nur fürs Testen, kein Verhaltensänderung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:11:17 +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
5f15946776 fix(compose): point mana-auth SYNC_DATABASE_URL at mana_sync, not mana_platform
`sync_changes` lives in DB `mana_sync` (the dedicated sync engine
database), not in `mana_platform` where mana-auth's other queries land.
The compose env had this miswired since SYNC_DATABASE_URL was first
introduced — F4's bootstrapUserSingletons (`c07db300b`) ran into
"relation sync_changes does not exist" but its fire-and-forget caller
swallowed the failure silently.

Punkt 3's explicit `/api/v1/me/bootstrap-singletons` endpoint
(`099cac4a0`) surfaced the misconfig as a user-visible 500 on first
real boot, which is how it got caught.

This also unbreaks user-data.ts (GDPR data summary entity counts +
account deletion's sync-row cleanup) which was returning 0 / no-op
for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:26:13 +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
Till JS
c94b67395a i18n(sleep): add namespace JSONs (de/en/es/fr/it)
Adds list_view sub-namespace covering log CTA, last-night card (label +
edit + interruptions), week chart heading, 5 stat labels (avg duration/
quality, sleep debt, consistency, streak), heatmap title interpolation,
hygiene-correlation card, log/hygiene action buttons.

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