Commit graph

1734 commits

Author SHA1 Message Date
Till JS
45790ffbb8 refactor(mana): rename inventar → inventory across the codebase
The workbench-registry app id 'inventar' did not match its
@mana/shared-branding MANA_APPS counterpart 'inventory', so the tier-
gating join in apps/web/src/lib/app-registry/registry.ts silently
failed for the inventory module — it fell into the "no MANA_APPS
entry, default visible" fallback and was effectively un-gated. The
codebase had also voted overwhelmingly for 'inventar' (53 files) vs
'inventory' (3 files in shared-branding), so the long-standing
mismatch was just bookkeeping debt waiting to bite.

Pre-release, no live data, so the cleanest fix is to align everything
on the English 'inventory':

- Workbench-registry id, module.config.ts appId, module folder, route
  folder and i18n locale folder all renamed via git mv
- Standalone apps/inventar/ workspace package renamed
- All imports, store identifiers (InventarEvents → InventoryEvents,
  INVENTAR_GUEST_SEED, inventarModuleConfig), i18n keys and href/goto
  paths follow the rename
- The German display label "Inventar" is preserved everywhere it is a
  user-visible string (page titles, i18n values, toast labels)
- Dexie table prefixes (invCollections, invItems, …) are unchanged
- Drive-by fix: ListView.svelte was querying non-existent
  inventarCollections/inventarItems tables — corrected to the actual
  invCollections/invItems names from module.config
- The "inventar ↔ inventory id mismatch" workaround comment in
  registry.ts is removed since the mismatch no longer exists

module-registry.ts also picks up the user's parallel newsModuleConfig
addition because both edits land in the same import block — keeping
them split would have left the build in an inconsistent state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:50:24 +02:00
Till JS
51f408755c fix(api/who): use /v1/chat/completions path for mana-llm
The who module's chat endpoint was returning 502 to the browser
because mana-api called /api/v1/chat/completions on mana-llm and
got 404 — mana-llm exposes the OpenAI-compatible /v1/chat/completions
path with no /api/ prefix.

This is the same bug research had until commit 63a91e36a fixed its
path. The chat module (apps/api/src/modules/chat/routes.ts) still
has the wrong path — flagged as a follow-up.

Diagnostic from inside the mana-api container:
  /v1/chat/completions       → 422 (right path, empty body)
  /api/v1/chat/completions   → 404 (wrong path)

mana-api log line that flagged it:
  who.llm_non_200 status:404

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:48:09 +02:00
Till JS
23f13d7139 test(mana/web/nutriphi): cover mealMutations.update
Five new cases against fake-indexeddb covering the new update mutation:

  - patches description and re-encrypts (verified via ENC_PREFIX wire
    format check + absence of original AND new plaintext in the blob)
  - patches numeric nutrition fields (stays plaintext for aggregation)
  - partial update only touches the supplied fields (mealType change
    does not zero out nutrition)
  - bumps updatedAt
  - throws on missing id

Total nutriphi suite is now 16/16 cases, ~50ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:45:35 +02:00
Till JS
4fd6a5cc77 feat(mana/web/nutriphi): meal detail page + foods breakdown + thumbnail-aware lists
New /nutriphi/[id] route — the missing endpoint of the photo workflow.
Loads the meal via inline useLiveQueryWithDefault(loadMealById, ...) so
the closure captures page.params.id directly (planta DetailView pattern).

Detail page features:
  - Full-resolution photo, click-to-expand lightbox modal
  - All six nutrient cards with the same color tokens as the dashboard
  - "Erkannte Bestandteile" — list of AI-identified foods (name +
    quantity + kcal) so users can see what Gemini actually parsed
  - Inline edit form (mealtype + description + 6 nutrient inputs),
    persists via mealMutations.update
  - "🔄 Erneut analysieren" for photo meals — calls analyze on the
    stored URL and overwrites description + nutrition without
    re-uploading the file
  - Two-stage delete confirm

Add page (add/+page.svelte):
  - Captures upload.thumbnailUrl + analysis.foods after KI analysis
  - Persists both via the extended createFromPhoto signature
  - Shows the foods breakdown card under the confidence badge so users
    see the parse before saving (closes the trust gap on low-confidence
    runs)

List pages (Heute + History):
  - Switch to photoThumbnailUrl ?? photoUrl for the row image — saves
    bandwidth on the most-rendered surface
  - Each meal row is now a link to /nutriphi/[id]
  - History row layout split into <a> + sibling delete button so the
    delete click doesn't bubble through navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:45:21 +02:00
Till JS
de4f766b06 feat(mana/web/nutriphi): extend meal schema (foods + thumbnail) + update mutation
Schema additions on LocalMeal:
  - photoThumbnailUrl: pre-generated mana-media thumbnail URL, used in
    list views to save bandwidth (full photoUrl stays for the detail
    view + lightbox)
  - foods: AnalyzedFood[] (name / quantity / calories) — Gemini Vision
    already returns this breakdown but the previous flow threw it away
  - new AnalyzedFood type exported from the barrel

Encryption registry:
  - meals encrypted allowlist now includes 'foods' (food names are
    user content; aes.ts JSON-stringifies arrays before wrap, so an
    array value works the same as a string)
  - registry comment updated to enumerate which photo fields stay
    plaintext and why

New mutation: mealMutations.update(id, dto) for inline meal edits.
Patches only the supplied fields, runs encryptRecord on the partial
update so encrypted columns stay encrypted, then re-decrypts the merged
row to return a plaintext snapshot.

queries.ts: new loadMealById(id) helper used by the detail page's
inline useLiveQueryWithDefault wrapper (matches the planta DetailView
pattern of capturing the route param directly in the closure).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:44:55 +02:00
Till JS
c7fd9369c9 test(mana/web/nutriphi): integration tests for meal mutations + encryption
11 cases against fake-indexeddb covering:

  - mealMutations.create: persistence, encryption allowlist (description
    encrypted, nutrition / mealType / structural fields plaintext),
    decryptRecord round-trip, plaintext snapshot return value,
    default-date and explicit-date paths
  - mealMutations.createFromPhoto: inputType=photo, photoMediaId /
    photoUrl plaintext, snapshot includes photo fields
  - mealMutations.delete: stamps deletedAt + updatedAt without
    physically removing the row (sync needs the tombstone)

Setup mirrors planta/mutations.test.ts: real Web Crypto via
generateMasterKey + MemoryKeyProvider, the same trigger / funnel-tracking
mocks the planta tests use, encryption verified through the ENC_PREFIX
wire-format check rather than mocking aes.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:14:24 +02:00
Till JS
189249ba01 feat(mana/web/nutriphi): photo capture + AI meal recognition flow
Wires the new backend endpoints into the unified Mana app and rebuilds
the meal-add page around two modes:

  - Text mode: free-text description + optional " KI-Vorschlag" button
    that runs Gemini on the description and prefills all six nutrient
    fields. The badge auto-clears if the user edits the description so
    stale estimates can't be silently saved.

  - Foto mode: file picker (accept=image/*, capture=environment for
    mobile camera) → preview → upload to mana-media → Gemini Vision on
    the stored URL. Result prefills the same form fields for review.
    Re-analyze without re-upload is supported.

Both modes show a confidence badge (green ≥50 %, yellow with a "prüfen"
warning below). Save is disabled in foto mode until the upload+analysis
has completed, so a meal can never be persisted with a dangling photo
reference.

New module files:
  - api.ts          server-only client (uploadMealPhoto, analyzeMealPhoto, analyzeMealText)
  - mutations.ts    mealMutations.create / .createFromPhoto / .delete + photoMutations
                    keeps the encryption pattern explicit (clone → encrypt → write,
                    return plaintext snapshot)

Touched:
  - queries.ts      propagate photoMediaId/photoUrl through toMealWithNutrition
  - index.ts        export the new mutations + types
  - registry.ts     extend the meals comment to document why nutrition,
                    photoMediaId, photoUrl and confidence stay plaintext
  - +page.svelte / history/+page.svelte    show 64×64 / 48×48 thumbnail
                    + 📷 indicator for photo-mode meals

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:14:07 +02:00
Till JS
693d20edd1 refactor(api/nutriphi): split photo flow into /photos/upload + /analysis/photo
Mirror the planta two-step pattern: a FormData upload endpoint that
returns mediaId/publicUrl from mana-media, and a separate Gemini Vision
analysis endpoint that takes a photoUrl. Drops the base64 inline path
and the half-finished parallel-upload kludge in the old combined route.

Why: the old endpoint was wired neither in the frontend nor used
elsewhere, and the combined base64+upload+analyze design made it
impossible to show the photo to the user before AI ran or to re-analyze
without re-uploading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:13:45 +02:00
Till JS
e6564cfc96 fix(mana/web): add standard mobile-web-app-capable meta tag
Chrome deprecates the apple-prefixed meta and now logs a warning
asking for the standardized mobile-web-app-capable equivalent. Adds
the standard tag alongside the apple one so iOS Safari keeps working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:08:12 +02:00
Till JS
f0faae0fb9 feat(mana/web): same-origin proxy for /api/v1/who/* → mana-api
Replaces the cross-origin call to https://mana-api.mana.how with a
SvelteKit catch-all server route that proxies internally to the
mana-api container over the docker network.

Why
---
The mana-api.mana.how cloudflared route was added as part of the
production deploy of apps/api, but reloading the cloudflared
LaunchDaemon to pick up the new ingress rule needs sudo. The deploy
automation runs unattended (no interactive password prompt), so
the cloudflared route ends up registered with Cloudflare DNS but
not yet served by the local tunnel — every browser request to
mana-api.mana.how gets a 404 from the catch-all rule until someone
manually restarts the daemon.

Same-origin proxy through mana-web sidesteps the whole problem:

  browser → cloudflared → mana-web (mana.how) → mana-api (docker net)

mana.how is already routed, mana-web is already up, mana-api is
already on the same docker network — no new cloudflared work
needed. The deploy is now fully sudo-free and self-contained.

What's in this commit
---------------------
  routes/api/v1/who/[...path]/+server.ts (NEW)
    Catch-all SvelteKit handler. Forwards GET/POST/PUT/DELETE to
    http://mana-api:3060/api/v1/who/<path> with the Authorization
    header from the incoming request. 30s timeout, body streamed
    through, status + content-type passed through 1:1, errors
    surface as 502 so DevTools clearly distinguishes "proxy
    failed" from "handler crashed".

  modules/who/stores/games.svelte.ts
    Drop the getManaApiUrl() import. API_BASE is now the constant
    string '/api/v1/who' — same-origin, no env injection needed.

  modules/who/ListView.svelte
    Same change for the deck-catalogue fetch on mount.

The MANA_API_INTERNAL_URL env var on the proxy lets the upstream
hostname be overridden for local-dev use (default
http://mana-api:3060 matches the docker compose service name).

Trade-off: one extra hop (mana-web in the middle) for every
request. Measured in single-digit ms over the bridge network so
the practical cost is invisible. The big win is the sudo-free
deploy.

Pattern can be reused for the other apps/api modules as their
compute features come online in production — same shape, just
swap [...path] segment to /api/v1/calendar/[...path],
/api/v1/picture/[...path], etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:06:04 +02:00
Till JS
ab0ca99239 feat(mana/web): app picker — autofocused search + alphabetical order
The "App hinzufügen" picker now sorts available apps alphabetically by
their (i18n-resolved) display name and shows an autofocused search input
above the list to filter quickly. Enter selects the first match.
PickerOverlay gains a `subheader` snippet slot so other pickers can
embed their own controls between header and list without having to
re-implement the shell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:37:04 +02:00
Till JS
41c705a303 feat(mana/web): per-module icons + wire workbench title link
Adds an optional icon field to AppDescriptor and assigns a Phosphor icon
to all 33 registered apps (CheckSquare for todo, Calendar for calendar,
AddressBook for contacts, …). AppPage now passes both the icon and
titleHref={`/${appId}`} to PageShell, so workbench cards show the
module's icon next to the now-clickable title instead of the generic
color dot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:28:01 +02:00
Till JS
a130f8e4c0 feat(mana/web): clickable page titles open route in new tab
PageShell gains an optional titleHref prop — when set, the header title
renders as an <a target="_blank"> with hover underline. Also wires this
into the homepage app gallery (shared-ui/AppsPage): the grid card title
is now an anchor to /{app.id}, while the rest of the card still opens
the existing detail modal. Card converted from <button> to role=button
so the nested anchor is valid HTML.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:27:51 +02:00
Till JS
2f00d9c5d3 feat(memoro): show title source label below the title input
Mirror the "Voxtral via mana-stt" label that already sits under the
transcript: a small italic line directly below the title input
showing which tier (and roughly which model) generated the title.
This way the user can see at a glance whether the title came from
the local rules engine, from Gemma 4 in their browser, from
gemma3:4b on the Mana server, or from Google Gemini — and can
decide whether to keep it or rewrite manually.

Storage:

  apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts
    - When applying a completed title task, the watcher now also
      stamps memo.metadata.titleSource with the LlmTier string
      ('none' | 'browser' | 'mana-server' | 'cloud') from the queue
      row's `source` field. Stored in the existing plaintext metadata
      object — no encryption needed (the tier name isn't sensitive
      and the encryption registry for memos only covers
      title/intro/transcript). Existing metadata fields are
      spread-preserved so we don't accidentally wipe STT failure
      markers etc.

Manual override clears the marker:

  apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts
    - memosStore.update() now detects when `title` is in the diff
      and clears `metadata.titleSource` so the DetailView stops
      showing "via Mana-Server (gemma3:4b)" for a title the user
      typed themselves. Only fires when title is actually present
      in the update payload — non-title updates leave metadata alone
      so we don't blow away other markers.

Display:

  apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte
    - New TITLE_SOURCE_LABELS map gives each tier a human-readable
      label that surfaces the actual model name where known:
        none        → "Lokal (regelbasiert)"
        browser     → "Auf deinem Gerät (Gemma 4)"
        mana-server → "Mana-Server (gemma3:4b)"
        cloud       → "Google Gemini"
      We deliberately don't reuse @mana/shared-llm's tierLabel()
      because the model name is more informative than the abstract
      tier in this UX context.
    - $derived `titleSourceLabel` reads memo.metadata.titleSource
      and validates it via an isLlmTier type guard. Returns null
      (→ no label rendered) when:
        * the entity hasn't loaded yet
        * a title task is currently in flight (titleIsGenerating)
        * the title input is currently focused (user is editing)
        * the metadata field is missing or not a known tier value
    - New `<div class="source-label title-source-label">` slot
      between the title-row and the properties block, with a small
      CSS override (.title-source-label) for a tighter top gap and
      a slight left indent so it visually lines up under the input
      text rather than under the input border.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:19:59 +02:00
Till JS
7fa3afcdc7 fix(mana/web/sync): push fresh writes immediately via listener bridge
createUnifiedSync exported onPendingChange but nothing ever called it,
so the Dexie hook in database.ts recorded _pendingChanges rows that
the sync engine never heard about. Live writes only ever drained on
the next page reload (via drainLeftoverPending). Observed live as
fresh calendar/timeblocks writes piling up in _pendingChanges with
zero POST traffic to sync.mana.how.

Add a listener bridge: database.ts exposes setPendingChangeListener,
trackPendingChange invokes it after each successful _pendingChanges
insert, and sync.ts registers schedulePush (gated on a known channel)
inside startAll. stopAll clears the listener so a torn-down sync
engine can't get re-triggered by a stale callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:13:25 +02:00
Till JS
6cbb1f64d0 fix(api/Dockerfile): switch builder stage to node:20-alpine
oven/bun:1 doesn't ship with npm or pnpm, so the previous
`RUN npm install -g pnpm@9.15.0` failed with `/bin/sh: 1: npm: not
found` on the first Mac Mini build. Bun's own install command
doesn't honor pnpm-workspace.yaml, so we can't use it as a drop-in.

Switch the builder stage to node:20-alpine which has npm built in,
install pnpm there, resolve the workspace graph, then COPY the
finished tree into the bun runtime stage. The runtime stage stays
on oven/bun:1 — bun handles pnpm's node_modules/.pnpm symlink farm
natively, so the workspace layout works the same as it does on a
developer machine.

Tested locally: `docker compose -f docker-compose.macmini.yml build
mana-api` now succeeds through the install stage. The runtime
stage is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:10:59 +02:00
Till JS
7750c46a12 feat(api): production deploy — Dockerfile + docker-compose service
Adds the missing production deployment artifacts for the unified
apps/api Hono/Bun server. Until now apps/api was code-only — built
during the consolidation sweep but never wired into the Mac Mini
compose stack, so all 17 product modules that depend on it
(calendar, todo, picture, planta, nutriphi, news, traces, presi,
music, contacts, storage, context, guides, research, chat, moodlit,
who) effectively had no backend in production. The frontend modules
shipped, but their compute calls fell through to localhost:3060 in
the browser and just failed.

This commit fixes the gap.

apps/api/Dockerfile (NEW)
-------------------------
Multi-stage Bun build that runs from the monorepo root so the four
workspace dependencies (@mana/shared-hono, @mana/shared-logger,
@mana/shared-storage, @mana/media-client) actually resolve. Builder
stage installs via pnpm with the --filter @mana/api... selector to
keep the install graph minimal; runtime stage copies the resulting
workspace tree (including the pnpm symlink farm) and runs the entry
script with bun directly — no compile step, since bun handles
TypeScript natively.

@mana/media-client lives under services/mana-media/packages/client,
not packages/, so the COPY path is the awkward
services/mana-media/packages/client → ./services/mana-media/packages/
client mirror to keep the workspace layout intact.

Healthcheck hits /health every 30s with a 15s start period — same
shape as the other Bun services in this compose file.

docker-compose.macmini.yml — new mana-api service
-------------------------------------------------
Slotted between glitchtip-worker and the games section. Build
context is the monorepo root (`.`) because the Dockerfile needs the
workspace tree. Container name `mana-api`, image `mana-api:local`,
mem_limit 384m (higher than the smaller Bun services because the
unified server holds 17 modules' route definitions + Drizzle schema
caches in memory).

Environment wires up everything apps/api needs:
  - MANA_AUTH_URL → mana-auth:3001 for JWT validation
  - MANA_LLM_URL → mana-llm:3025 for chat / picture / who LLM calls
  - MANA_SEARCH_URL → mana-search:3012 for guides / research
  - MANA_CREDITS_URL → mana-credits:3002 for credit validation
  - MANA_MEDIA_URL → mana-media:3011 for image uploads
  - DATABASE_URL → mana_platform Postgres for the few server-side
    state stores (research_results, presi share-links, traces guides)
  - MANA_SERVICE_KEY → for the credit/auth service-to-service calls
  - LOGGER_FORMAT=json → structured logs for grafana ingestion
  - CORS_ORIGINS=https://mana.how → only the unified web origin
    needs access, the standalone game frontends don't call this

Port 3060 is exposed on the host so cloudflared can route
api.mana.how → mana-api:3060 (separate Mac Mini side step, not
in this commit).

docker-compose.macmini.yml — mana-web wiring
--------------------------------------------
Two new env vars:
  PUBLIC_MANA_API_URL=http://mana-api:3060
  PUBLIC_MANA_API_URL_CLIENT=https://api.mana.how

The hooks.server.ts injection plumbing for window.__PUBLIC_MANA_API_URL__
already existed (added in an earlier sweep but never had a value to
inject). The CSP connect-src list and the SSR injection script tag
also already include PUBLIC_MANA_API_URL_CLIENT — so once the env
arrives, the existing client-side getManaApiUrl() helper picks it
up automatically.

mana-web also gets a depends_on entry on mana-api with
condition: service_healthy so the web container doesn't start
serving requests against a dead API.

Verification
------------
docker compose -f docker-compose.macmini.yml config validates
cleanly (no YAML errors). Image build is NOT exercised in this
commit — that happens on the Mac Mini via build-app.sh after the
push lands.

Out of scope for this commit (Mac Mini side, manual steps):
  1. ssh mana-server, git pull
  2. ./scripts/mac-mini/build-app.sh mana-api  (first build, ~3-5 min)
  3. ./scripts/mac-mini/build-app.sh mana-web  (rebuild with new env)
  4. cloudflared route: add api.mana.how → mana-api:3060 to
     ~/.cloudflared/config.yml and `systemctl restart cloudflared`
  5. Test https://api.mana.how/health from anywhere
  6. Test https://mana.how/who in a browser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:08:59 +02:00
Till JS
ad618930b5 chore(planta): remove orphan apps/planta/packages/shared
The package was a leftover from the per-product Planta backend that
got consolidated into apps/api a while back. Repo-wide grep for
@planta/shared returns zero matches — it had no consumers.

- Delete the four files (package.json, src/index.ts, src/types/index.ts,
  tsconfig.json)
- Update apps/planta/CLAUDE.md to reflect the cleanup (the previous
  note pointed at a refactoring audit doc that already tracked it)
- Refresh pnpm-lock.yaml so the workspace member is no longer pinned

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:06:11 +02:00
Till JS
b68fcc8fd1 fix(mana/web/planta): /planta routes — layout fix, i18n, nullability, button nesting
- Add /planta/+layout.svelte that provides every live-query context
  the legacy routes already reference via getContext (plants,
  plantPhotos, wateringSchedules, wateringLogs, plantTags, tags).
  Without this layout the legacy routes would crash at runtime with
  "Cannot read properties of undefined (reading 'value')" — they had
  always relied on a provider that did not exist anywhere in the repo.
- Replace every hardcoded German label across +page.svelte,
  [id]/+page.svelte, add/+page.svelte and tags/+page.svelte with
  $_('planta.*') calls so the locale switcher actually changes the
  copy. Health/light/humidity helper maps converted from German maps
  to switch + i18n lookups.
- Fix the 4 type errors in [id]/+page.svelte caused by SvelteKit's
  $page.params.id being string | undefined: coerce to '' so the
  helpers stay strictly typed and "missing id" still resolves to
  "not found".
- Fix the SSR hydration warning on /planta from a <button> nested
  inside another <button> in the plant grid. Replaced the outer
  card with <div role="link" tabindex="0"> + Enter/Space keydown
  handler so the inner "water now" button is structurally legal.
- formatDate calls drop the hardcoded de-DE locale and use the
  browser locale (undefined) instead.
- Toast notifications on every mutation in these routes so failures
  are user-visible (handleWater, handleDelete, savePlant).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:05:56 +02:00
Till JS
60fedbb611 feat(mana/web/planta): photo upload, AI identification, tags, watering history, i18n + tests
Brings the planta module to production-ready state:

- Photo upload UI in the workbench DetailView (file picker, primary
  selection, removal, hover overlay) wired to /api/v1/planta/photos/upload
- AI plant identification trigger that calls /analysis/identify on the
  primary photo and shows a result card with apply-to-plant CTA;
  applyIdentification only fills empty fields by default to avoid
  clobbering user edits
- Tag picker (chip UI + dropdown) backed by plantTagOps junction
- Watering history list (last 5 logs) in DetailView
- Full i18n: every locale (de/en/es/fr/it) now has plant/list/photo/
  identify/errors/success keys; ListView and DetailView consume them
  via $_('planta.*') instead of hardcoded German
- Toast notifications on every mutation success/failure path
- mutations.ts refactored: methods now throw on failure instead of
  swallowing errors and returning null, so callers can surface them
- New api.ts client for the two server-only operations (upload, identify)
- New photoMutations + plantMutations.applyIdentification helpers
- quick-input-adapter type fix: stop referencing the non-existent
  parsed.species field; create plants through plantMutations.create
  so encryption + timestamps run, and decrypt names before substring
  search
- 20 new tests:
  - queries.test.ts (13 pure-function tests for getDaysUntilWatering /
    isWateringOverdue / getScheduleForPlant / getLogsForPlant)
  - mutations.test.ts (7 fake-indexeddb integration tests for
    wateringMutations.logWatering — log appended, schedule re-anchored,
    soft-deleted schedules skipped, multi-call uniqueness)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:05:13 +02:00
Till JS
b83e8d6d92 fix(mana/web): seed pending-changes badge count on mount
Some checks are pending
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
CI / Build mana-media (push) Blocked by required conditions
CI / Build mana-credits (push) Blocked by required conditions
CI / Build mana-web (push) Blocked by required conditions
CI / Build chat-backend (push) Blocked by required conditions
CI / Build chat-web (push) Blocked by required conditions
CI / Build todo-backend (push) Blocked by required conditions
CI / Build todo-web (push) Blocked by required conditions
CI / Build calendar-backend (push) Blocked by required conditions
CI / Build calendar-web (push) Blocked by required conditions
CI / Build clock-web (push) Blocked by required conditions
CI / Build contacts-backend (push) Blocked by required conditions
CI / Build contacts-web (push) Blocked by required conditions
CI / Build presi-web (push) Blocked by required conditions
CI / Build storage-backend (push) Blocked by required conditions
CI / Build storage-web (push) Blocked by required conditions
CI / Build telegram-stats-bot (push) Blocked by required conditions
CI / Build nutriphi-backend (push) Blocked by required conditions
CI / Build nutriphi-web (push) Blocked by required conditions
CI / Build skilltree-web (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build zitare-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
The OfflineIndicator badge shows networkStore.pendingCount, which is
only refreshed inside unifiedSync.onStatusChange. That callback fires
on transitions only — so on a fresh tab where sync stays idle, the
badge sticks at the last persisted value (or 0). Observed live as
"13 pending" while _pendingChanges actually held 27 rows.

Extract refreshPendingCount as a local helper and call it once right
after startAll() to seed the badge. The transition-driven refresh
inside onStatusChange now reuses the same helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:25:06 +02:00
Till JS
be8c0482b7 fix(mana/web/sync): drain leftover pending changes on startup
startAll() registered channels but never kicked a push for changes
that survived across page reloads. schedulePush only fires from the
Dexie hook on fresh writes, so any pending row from a previous session
sat in _pendingChanges until the user happened to mutate the same
table again — observed live as 27 pending across mana/memoro/places
that never reached the server despite a healthy sync route.

Add drainLeftoverPending() called once at the end of startAll(): scan
_pendingChanges for distinct appIds and schedulePush each registered
channel. Fire-and-forget; errors swallowed because the push retry
path already handles failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:24:51 +02:00
Till JS
0450c86527 fix(shared-llm): SSE shape diagnostics + simpler title prompt + fragment detection
User test on the mana-server tier showed Ollama gemma3:4b returning
LITERALLY empty content for the title task, which is much weirder
than the small browser model misbehaving. Three layered fixes plus
diagnostics that will tell us what's actually happening over the
wire next time.

1. remote.ts: SSE diagnostics + liberal field shape

   The mana-llm /v1/chat/completions endpoint claims OpenAI
   compatibility, but different upstream providers (Ollama, OpenAI,
   Gemini) wrap their token text in different field paths inside
   the SSE delta. Be liberal in what we accept:
     - choice.delta.content   (canonical OpenAI)
     - choice.delta.text      (some Ollama-compat shims)
     - choice.message.content (non-streaming response embedded in stream)
     - choice.text            (legacy completion API)

   Plus: count totalFrames + dataFrames + capture firstFrameRaw +
   firstFrameParsed during the stream. When `collected` is empty at
   the end of the stream, dump all of that to console.warn so the
   next test session shows us exactly what mana-llm is sending. This
   is the only reliable way to debug "empty completion" without a
   network sniffer in the user's browser.

2. generate-title.ts: drop few-shot, use simple system+user prompt

   The previous few-shot prompt with three `Aufnahme: "..."\nTitel: ...`
   examples was apparently too much for Ollama gemma3:4b on the
   mana-server tier — it returned literal "" for reasons we don't
   fully understand (chat-template confusion with the embedded
   quotes? multi-section format? some quirk of how mana-llm formats
   the messages for Ollama?). Either way, the failure mode is clear.

   Replace with a minimal two-message format:
     - system: "Du erzeugst einen kurzen Titel (3-5 Wörter)..."
     - user: <transcript>
   Same instruction, much simpler shape. Bumped maxTokens 24 → 32
   to give the model breathing room.

3. generate-title.ts: rules fallback detects sentence fragments

   Even when the LLM fails and we fall through to runRules, the
   previous heuristic for medium-length transcripts (10-20 words)
   would extract the first 7 words verbatim — which for a typical
   "Eine kleine Testaufnahme um zu sehen ob alles funktioniert" memo
   produces "Eine kleine Testaufnahme, um zu sehen, ob" as the
   "title". That's a sentence fragment ending mid-thought, not a
   title. Worse than "Memo vom 9. April 2026".

   Add a "looks like a sentence fragment" heuristic: if the last
   word of the extracted slice is a German stop-word or article
   (und/oder/wenn/ob/zu/um/der/die/das/ein/...) the result is
   clearly mid-clause. In that case fall through to dateLabel()
   instead of writing the fragment.

   Stop-word list is curated to 30 entries — common conjunctions,
   articles, prepositions, auxiliaries. Not exhaustive but catches
   the typical "first 7 words of a German sentence" failure mode.

After this commit lands, the next test will surface in the console
EITHER:
  - the actual delta shape mana-llm is using (so we know if our
    parser is wrong or if the model is genuinely silent)
  - a real LLM-generated title (if the simpler prompt worked)
  - "Memo vom <date>" via the rules fallback (if the LLM still
    fails but the rules fragment detection caught the bad slice)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:12:13 +02:00
Till JS
f24438f778 feat(mana/web): who module — frontend (game store + UI + routes)
Client side of the who module. Standard Mana module pattern: types,
collections (Dexie), queries (live), store (mutations), UI components,
routes. Plus three integration points (data layer registries).

Module files
------------
  types.ts
    Two Dexie record interfaces (LocalWhoGame, LocalWhoMessage) and
    matching view types. Server response shapes (WhoChatResponse,
    WhoRandomResponse, WhoGuessResponse) live here too so the store
    and UI both type-check against the same wire contract.

  collections.ts
    Dexie table accessors. No guest seed — the picker handles empty
    state directly.

  queries.ts
    Three liveQueries (allGames$, gameByIdLive, messagesForGameLive)
    that decrypt the encrypted-at-rest fields before returning view
    types. The messages query uses the [gameId+createdAt] composite
    index for ordering. toWhoGame / toWhoMessage converters bridge
    the BaseRecord-extended local types to the public view types.

  module.config.ts
    Standard ModuleConfig: appId='who', tables=[whoGames as 'games',
    whoMessages as 'messages']. The syncName remap means the unified
    Dexie table whoGames syncs to mana-sync's `games` collection
    under appId 'who' — keeps the wire format clean.

  stores/games.svelte.ts
    The mutation surface. Five public methods:
      - start(deckId)        → POST /who/random + insert LocalWhoGame
      - sendMessage(id, txt) → optimistic insert + POST /who/chat +
                               insert NPC reply + (on win) flip status
      - submitGuess(id, txt) → POST /who/guess + (on match) flip
      - surrender(id)        → status=surrendered + finishedAt
      - setNotes(id, notes)  → encrypted post-game notes
      - deleteGame(id)       → soft-delete game + cascade messages
    All writes go through encryptRecord for encrypted-at-rest fields.

UI components
-------------
  ListView.svelte
    Module landing page. Header + 4 deck cards (loaded from
    GET /api/v1/who/decks on mount) + past-games list. Picking a
    deck calls store.start() and navigates to the play view. Past
    games are clickable (read-only for finished games) and
    deletable.

  views/PlayView.svelte
    The chat-loop screen. Header with deck/difficulty + back button
    + Tippen/Aufgeben actions while playing. Scrollable message
    area with bubbles (user purple-tinted, NPC white-tinted).
    Textarea input with Enter-to-send + sending disabled state.
    On reveal: result banner with "Erraten in N Nachrichten!" and
    the resolved name. Post-game: input area swaps to a notes
    textarea with debounced auto-save. Explicit guess modal as
    fallback when the LLM forgets to emit the sentinel.

Routes
------
  /(app)/who                 → ListView wrapper
  /(app)/who/play/[gameId]   → PlayView wrapper, $page.params.gameId

Registry plumbing
-----------------
  database.ts
    Two new Dexie tables in version(1):
      whoGames: 'id, status, deckId, startedAt, finishedAt, [status+startedAt]'
      whoMessages: 'id, gameId, sender, createdAt, [gameId+createdAt]'

  module-registry.ts
    Imports whoModuleConfig and adds to MODULE_CONFIGS. The sync
    engine picks up the appId/table mapping automatically — no
    edits needed in sync.ts.

  crypto/registry.ts
    Two entries:
      whoGames:    { enabled: true, fields: ['revealedName', 'notes'] }
      whoMessages: { enabled: true, fields: ['content'] }
    All other fields stay plaintext for index/sort/filter.

Closes Phase A.2 / A.3 / A.4 / A.5 of docs/WHO_MODULE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:10:34 +02:00
Till JS
74b5808496 feat(api): who module — LLM character-guessing endpoint cluster
Server side of the who module. Three endpoints under /api/v1/who/*:

  POST /chat
    Hot path. Body: { gameId, characterId, message, history[] }.
    Looks up character by id (server-side only — clients never see
    personalities), builds a system prompt instructing the LLM to
    roleplay the figure WITHOUT revealing its name and to append
    [IDENTITY_REVEALED] when the player has guessed correctly,
    forwards to mana-llm. Response: { reply, identityRevealed,
    characterName? } — characterName only present on win.

    Same credit pattern as chat module: validateCredits + consume
    after the LLM call succeeds. Operation 'AI_WHO', cheap (0.1
    credit) for local models, 5 for cloud.

  POST /random
    Picks a random character from a deck and returns just the id +
    category + difficulty. Frontend uses this to start a new game
    without ever knowing the personality pool. Server-side
    randomness so a determined attacker can't predict picks.

  POST /guess
    Explicit "I think it's X" submission. Fallback path for when
    the LLM forgets to emit the sentinel even though the player
    clearly said the right name. Deterministic lowercase substring
    match against the canonical name (with diacritic stripping +
    last-name-only matching for unambiguous figures like "Tesla").

  GET /decks
    Public deck catalogue with counts and category labels. Zero
    sensitive data — never leaks names or personalities. Used by
    the picker UI on mount.

data/characters.ts holds 37 characters: the original 26 from
whopixels verbatim + 11 new for the antiquity / women / inventors
decks. Each entry is in one or more decks via a `decks` array, so
e.g. Marie Curie shows up in both `historical` and `women`. Adding
a new character is one entry.

The system prompt is the carefully-tested German prompt from the
original whopixels server.js — tells the LLM to respond in the
language the user writes, give subtle hints, never directly say
"I am X", and emit the sentinel only on a correct guess.

The explicit-guess matcher catches three patterns:
  1. Exact normalized match ("Marie Curie" === "marie curie")
  2. Last-name-only ("Curie" matches "Marie Curie")
  3. Guess-contains-name ("I think it's Marie Curie" → contains)

Closes Phase A.1 of docs/WHO_MODULE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:09:46 +02:00
Till JS
d8591b320b fix(generate-title): few-shot prompt + rolling cleanup + date label for short transcripts
User test on browser tier (Gemma 4 E2B) showed two compounding bugs:

  1. The LLM produces empty content. The cleanup chain strips it to ""
     and falls through to runRules.
  2. runRules takes the first 7 words of the transcript. For short
     voice memos like "So erneut eine kleine Testaufnahme hier"
     (6 words) that means the entire transcript becomes the title —
     not actually a title, just the recording verbatim.

User log:
  [memoro] enqueued title task ...
  [generateTitle] LLM returned empty after cleanup, falling back to rules
  [memoro-llm-watcher] writing title to memo X: "So erneut eine kleine Testaufnahme hier"

Three changes to fix the actual quality, not just the empty-string
symptom from the previous commit:

1. Rewrite the LLM prompt as few-shot

   Replace the previous "Du erstellst kurze Titel — kein Markdown,
   keine Anführungszeichen, keine Vorrede, kein Punkt am Ende" prompt
   (a wall of negative constraints that small instruct models like
   Gemma 4 E2B handle poorly) with a few-shot user-only message:

     Erstelle einen kurzen Titel (3-5 Wörter) für die folgende Aufnahme.

     Beispiel 1:
     Aufnahme: "Erinnere mich daran, morgen Vormittag den Müll
                rauszubringen, bevor die Müllabfuhr kommt."
     Titel: Erinnerung Müll rausbringen

     Beispiel 2: ... (Idee Präsentation Demo-Start)
     Beispiel 3: ... (Steuererklärung 2025)

     Aufnahme: "<user transcript>"
     Titel:

   Small instruct models complete the pattern much more reliably
   than they obey negative constraints. The expected continuation is
   just the title text, no punctuation, no markdown, no preamble.

2. Rolling cleanup that won't go to empty

   The previous cleanup chain (`.trim().replace(quotes).replace(dots).trim()`)
   could end up with "" if the model emitted only `.` or `**.**` or
   similar. Replace with a four-stage chain that picks the FIRST
   non-empty stage from the bottom up:

     trimmed     = result.content.trim()
     stripFences = first line only (kills any model rambling)
     stripQuotes = strip surrounding quotes/markdown markers
     stripDots   = strip trailing dots
     cleaned     = stripDots || stripQuotes || stripFences || trimmed

   This way "Test." → "Test" but `"."` → `"."` (kept as-is rather
   than stripped to empty). The runRules fallback only fires when
   the model truly emits nothing usable in any stage.

3. runRules is smarter about short transcripts

   For voice memos with ≤8 words in the first sentence, the "title"
   would just be the whole transcript echoed back. That's not useful.
   The new threshold: short transcripts get a date label instead
   ("Memo vom 9. April 2026"), longer ones still get the first-N-words
   snippet. The threshold is empirical — short voice memos benefit
   from a date marker, longer ones can spare a few words for a snippet.

   Extracted dateLabel() to a module-scope function so both rulesImpl
   (for empty/short transcripts) and the watcher's last-resort
   backstop can format dates consistently.

Diagnostic: log the RAW LLM output before cleanup so the next test
session shows exactly what Gemma is producing. If the model is still
emitting only punctuation despite the few-shot prompt, the log will
show `"\n"` or `"."` and we'll know the bug is in the inference path
rather than the cleanup.

After this commit, the user-visible result for a 6-word transcript
on the browser tier should be:
  - LLM produces something real ("Test der Sprachaufnahme") → write it
  - LLM produces nothing → rules → "Memo vom 9. April 2026"
  - both fail somehow → watcher's date backstop → same
  - never the verbatim transcript

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:03:16 +02:00
Till JS
ea8ca13d37 fix(mana/web): wrap bare var(--color-X) refs in hsl() across 19 files (P5)
These were latent rendering bugs: --color-X holds raw HSL channels at
runtime (set by createThemeStore), so a bare var(--color-foreground) is
not a valid CSS color value — the browser falls back to inherited and
the affected elements render with the wrong color (often invisible
text on the same-colored background).

Mechanical wrap of every bare reference in the affected files:
  var(--color-X)              → hsl(var(--color-X))
  var(--color-X, #fallback)   → hsl(var(--color-X))   (fallback dropped)
  color-mix(... var(--color-X) N%, transparent)
                              → hsl(var(--color-X) / 0.NN)

Also re-mapped two long-removed token names:
  --color-surface     → --color-muted    (subtle surface intent)
  --color-destructive → --color-error    (semantic alias)

190 refs across 19 files (habits, photos, notes, places, todo, cycles
helpers + their parent route shells). Brand-literal hex/rgba colors
left untouched (cycles pink, sport/category palettes, indigo→violet
gradients, photo placeholder gradients).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:02:33 +02:00
Till JS
9760269e9f fix(memoro): generateTitle empty-result fallback + watcher diagnostics
User test surfaced the actual auto-title bug: the entire pipeline
(enqueue → process → watcher) works correctly, but the task result
itself is empty:

  [memoro] enqueued title task { taskId, memoId }
  [memoro-llm-watcher] saw 1 done title task(s)
  [memoro-llm-watcher] writing title to memo XXX: ""
  [memoro-llm-watcher] applied + cleared row YYY

The watcher faithfully wrote "" to memo.title, the input placeholder
showed "Titel..." again, and we looked stuck. Three layered fixes so
this can't bite us in any execution path going forward.

1. generate-title.ts: extract rules logic + use it as runLlm fallback

   Pulled the deterministic first-sentence heuristic into a private
   `rulesImpl()` function so both `runLlm` and `runRules` can call
   it. runLlm now invokes rulesImpl as a fallback when the cleaned
   LLM output is empty. This catches the case where the model emits
   only punctuation, only special tokens, or only whitespace — all
   of which collapse to "" after my cleanup chain (`.trim()` → strip
   surrounding quotes/markdown → strip trailing dots → `.trim()`).

   The most likely real-world trigger: Gemma 4 occasionally emits a
   single `.` for short prompts that hit its over-strict
   "answer with ONLY the title" instruction. The cleanup turns
   "." into "" and we lose the result.

2. llm-watcher.svelte.ts: date-based backstop for any empty result

   Belt-and-suspenders: even if a future task implementation forgets
   the rules fallback, the watcher itself now guarantees a non-empty
   title. When `row.result.trim()` is empty, synthesize a label like
   "Memo vom 9. April 2026" from the memo's createdAt (or the
   current date if createdAt is also broken). The user always sees a
   real title — never an empty placeholder.

   Same write path otherwise (encryptRecord + memoTable.update +
   delete queue row), just with the guaranteed-non-empty value.

3. llm-watcher.svelte.ts: enhanced diagnostic logging

   The "writing title" log now includes `row.source` (which tier
   actually executed) and `row.attempts`, so the next time we see
   weird behavior we can tell at a glance whether it was the
   browser tier, the rules tier, or the server. The empty-result
   path logs `console.warn` (not info) with the raw result via
   JSON.stringify so we see exactly what came back ("", ".", "  ",
   undefined-coerced-to-string, etc.).

After this commit lands:
  - Tier 0 user: runRules returns at minimum "Ohne Titel" (its
    own fallback). The watcher writes that.
  - Browser tier with empty Gemma output: runLlm now falls through
    to rulesImpl which also can't return empty. The watcher writes
    the rules-tier output.
  - Any other freak case where the result is still empty: the
    watcher's date-based backstop kicks in. "Memo vom <date>".

So the user-visible "stuck on empty title" symptom is impossible in
all three layers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:54:06 +02:00
Till JS
0987b08185 style(mana/web): migrate (app) page-level routes to theme tokens (P5)
calendar, contacts, finance, spiral, todo route shells: bare var()
references → wrapped hsl(), broken rgba/hex fallback chains dropped.

DnD overlay (`.mana-drop-target-hover` / `-success`) is duplicated
inline in calendar/contacts/todo since it's a `:global()` rule each
route declares for itself; all three now read --color-primary +
--color-success for the drop animation instead of literal indigo/green.

finance: income=success, expense=error, type-toggle uses
--color-error/--color-success with /0.15 + /0.3 alpha modifiers.

spiral: indigo→violet stat highlight + app-bar gradient stay literal
(spiral's brand mark is the indigo→violet ramp, not the app theme
primary). Danger button now uses --color-error.

Skipped: rsvp/[token] (public landing, deliberate rose palette outside
the auth-gated chrome) and observatory (cosmic-scenes brand palette,
already established as brand-legitimate).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:43:24 +02:00
Till JS
526d92f41c fix(memoro): diagnostic logs + loading states + transcription source label
User reported three issues after the Phase 5 + the encryption-decrypt
fix landed:

  1. Auto-title still doesn't appear (placeholder "Titel..." stays empty)
  2. No loading state visible while transcription / title are in flight
  3. Transcript should say which STT engine produced it

This commit ships diagnostics for issue 1 and concrete UX for 2 + 3.

Issue 1 — diagnostics (no fix yet, root cause unknown):

  Add console.info logs at every step of the auto-title pipeline so
  the next test session surfaces exactly where it breaks:

  - memos.svelte.ts after llmTaskQueue.enqueue() succeeds:
      "[memoro] enqueued title task { taskId, memoId }"
  - memos.svelte.ts on enqueue failure:
      "[memoro] failed to enqueue title task: <err>"
  - memoro/llm-watcher.svelte.ts on subscribe:
      "[memoro-llm-watcher] starting subscription"
  - watcher's next handler when rows arrive:
      "[memoro-llm-watcher] saw N done title task(s)"
  - applyRow logs each step: drop / skip / write / consume

  Refactor: extract per-row logic into applyRow() so the next handler
  loop can wrap each row in try/catch — a single bad row won't crash
  the watcher and prevent later rows from being processed.

  Belt-and-suspenders startup sweep: run a one-shot manual sweep of
  done rows immediately after subscribing. Dexie liveQuery sometimes
  misses the first emission when the subscription is set up in the
  same microtask as a recent table update; the sweep catches any
  done rows that already exist from a previous tab session OR that
  were written between layout mount and subscription start.

  Encryption check fix: the previous skip-if-manual-title check
  read `memo.title?.trim()` after Dexie.get(), but Dexie reads
  return the ENCRYPTED row (no decrypt hook) — so memo.title is
  either null/undefined (no manual title) OR an `enc:1:...` blob
  (manual title set). Either way, presence-check is enough; no
  need to decrypt to know whether the user filled it in. The old
  code happened to work because trim() on a non-empty string
  returns truthy regardless. Comment now spells this out.

Issue 2 — visible loading states:

  apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte

  Transcript area now branches on processingStatus:

    - processing → "Wird transkribiert…" with three pulsing dots
                   (CSS @keyframes loadingPulse)
    - failed     → red error message + manual retry hint
    - completed + transcript → the transcript itself + source label
    - completed + no transcript → italic "Kein Transkript vorhanden."

  Title input placeholder swaps to "Titel wird generiert…" while a
  generateTitleTask for this memo is in pending or running state.
  The check uses a Dexie liveQuery against llmQueueDb.tasks via the
  [refType+refId] compound index, returning the most recent task row.
  Reactive — the placeholder switches back to plain "Titel…" the
  moment the watcher writes the title and deletes the queue row.

Issue 3 — transcription source label:

  Below the transcript: a small italic "Voxtral via mana-stt" label.
  Hardcoded to Voxtral because that's services/mana-stt's default
  model (DEFAULT_MODEL = "mistralai/Voxtral-Mini-3B-2507" in
  voxtral_service.py). If we ever route to Whisper or another model
  per-request, the label will need to come from the response payload
  rather than be hardcoded — Phase 5.5 work.

After this commit lands, the test loop is: record a memo, watch the
browser console for the [memoro] / [memoro-llm-watcher] log lines.
Whichever step is missing identifies the broken link.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:41:28 +02:00
Till JS
a7fbd29a67 style(mana/web): migrate photos/times/contacts module helpers to theme tokens (P5)
photos/PhotoCard + PhotoDetailModal: bare var() refs → hsl(var()), broken
fallbacks dropped. The lightbox backdrop stays explicit near-black — photo
viewing chrome is intentionally theme-neutral.

times/FocusCard: phase color (focus=red, break=green, idle=muted) reads
theme tokens via wrapped hsl() strings so the SVG ring tracks variants.
The bogus --color-input fallback is gone.

times/EntryItem: was referencing the long-removed shadcn aliases without
the --color- prefix (--border, --card, --foreground, --muted-foreground,
--primary, --input). Re-prefixed; --input → --background since we have
no separate input token. The delete button's text-red-500 / hover bg are
now --color-error so they track theme variants.

contacts/ContactPage: avatar + self-badge color-mix fallback chains
collapse to plain hsl(var(--color-primary) / 0.12).

The cycles pink #ec4899 birthday accent on the contact row stays literal
— it's a deliberate brand color, not theme intent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:37:29 +02:00
Till JS
d2e44c8b65 chore(packages): remove 2 zero-consumer config packages
Item #21 in the pre-launch audit suggested merging the four
config-y packages (shared-config, shared-tsconfig, shared-vite-config,
shared-drizzle-config) into a single @mana/build-config with
conditional exports. The first reality-check of the item counted
package.json declarations and reported 5 total consumer relationships.
A second reality-check while implementing — grep over actual .ts /
.svelte / .json imports — showed two of the four packages are dead:

  - packages/shared-config/ (598 LOC, 4 TS files)
    Declared in apps/mana/apps/web/package.json but never imported
    anywhere. Stale dep from before the consolidation.

  - packages/shared-tsconfig/ (5 JSON tsconfig presets)
    Zero references anywhere. Not extended by any tsconfig.json,
    not declared in any package.json. Pure Pre-Consolidation
    leftover.

The remaining two packages were left intact:
  - shared-vite-config (3 real consumers in vite.config.ts files)
  - shared-drizzle-config (1 real consumer in mana-media)

They cover different toolchains (Vite SSR config vs drizzle-kit
generator config) — merging them into a single build-config would
be cosmetic, not a real reduction in complexity. Audit's "merge to
1" goal was based on the inflated consumer count and is no longer
worth doing.

Verification:
  - pnpm install completes cleanly
  - apps/api type-check still 0 errors
  - packages/shared-hono type-check still 0 errors

Net: 4 → 2 config packages, ~700 LOC dead code removed.

Also closes item #26 (non-root pnpm-lock.yaml status) — already
done in commit 034a07d16, doc was just out of date. Audit is now
29/29 items fully processed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:35:40 +02:00
Till JS
5c41ebea33 style(mana/web/automations): migrate ListView to theme tokens (P5)
Wraps all `var(--color-X)` references with `hsl()` and routes the muted
backgrounds + borders through `--color-card` / `--color-border` instead
of the rgba-on-white fallbacks. The brand violet (#8b5cf6) automations
accent and the deliberate when/filter/then flow-step palette
(blue/amber/green) stay literal — they encode trigger/condition/action
semantics, not theme intent.

Last file from the original P5 ListView migration list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:34:32 +02:00
Till JS
5052926481 refactor(mana/web): migrate chrome/notification components to theme tokens (P5)
Six chrome-level UI components — modals, toasts and prompts that float
above the workbench — moved off hand-rolled #1e293b/#e5e7eb/#6366f1/etc.
literals onto theme tokens.

Files migrated:
  - RecoveryCodeUnlockModal — backdrop overlay (literal black/60),
    danger-state background → color-error
  - SessionWarning — warning toast bg → color-warning, dark text on the
    bright warning bg stays literal (intentional contrast pair)
  - SuggestionToast — primary CTA → color-primary, muted/error text →
    tokens. The toast itself keeps its dark literal bg by design (it's
    a floating notification, not a theme-aware surface)
  - SyncConflictToast — hover background → color-surface-hover
  - PwaUpdatePrompt — primary CTA was hardcoded indigo (#6366f1), now
    follows the active theme variant
  - auth/AuthRequiredModal — backdrop overlay literal, primary button
    text → color-primary-foreground

Backdrop overlays use literal `hsl(0 0% 0% / 0.6)` rather than a theme
token because semi-transparent black is the deliberate UI affordance
for "modal screen dimmer", not a theme-aware surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:27:09 +02:00
Till JS
c8da2267b7 fix(skilltree): define the missing branch accent colors
skilltree/types.ts has had `var(--color-branch-{intellect,body,creativity,
social,practical,mindset})` references for as long as I can grep, but those
CSS variables were never defined anywhere. Every skill in the gamified
tree was rendering inherited color (effectively invisible accent), making
the 6 branches visually indistinguishable.

Add the 6 colors as a new "domain accent" section in shared-tailwind/themes.css,
defined once at :root and never overridden by .dark or variant blocks
(they're brand-internal accents, not theme-aware — the same way cycles
keeps its brand pink literal).

  - intellect → blue (217 91% 60%) — knowledge, thinking
  - body      → red (0 84% 60%) — physical, energy
  - creativity → violet (271 91% 65%) — art, expression
  - social    → amber (38 92% 50%) — warmth, relationships
  - practical → teal (173 80% 40%) — craft, tools
  - mindset   → green (142 71% 45%) — calm, growth

Also update skilltree/types.ts to wrap the var() calls with hsl() per
the canonical convention (the values are now raw HSL channels).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:23:55 +02:00
Till JS
ddde284f17 fix(memoro): decrypt DetailView fields + stamp createdAt on memo create
Two pre-existing bugs in the memoro module that became visible after
the Phase 5 LLM auto-title work landed. Both are independent of the
Phase 5 framework — neither was introduced by it — but the auto-title
was the first feature to systematically write to memo.title, which is
when the broken read path stopped hiding behind always-null titles.

Bug 1: DetailView shows ciphertext instead of plaintext

  apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte
  passed `useDetailEntity({ table: 'memos', ... })` WITHOUT setting
  `decrypt: true`. The crypto registry has memos.{title, intro,
  transcript} marked as encrypted, so the inputs were binding to
  raw `enc:1:Ghj1eJV0zz4PgfRL...` ciphertext strings instead of
  plaintext. Nobody noticed before because:

    - title was always null (no UI path to set it until Phase 5)
    - intro is rarely used
    - transcript was the only visible encrypted field, and the
      garbled `enc:1:...` string in the transcript area was apparently
      attributed to "broken transcription" rather than "broken read"

  Add `decrypt: true` to the useDetailEntity options. Same flag the
  other Mana modules already use for their encrypted DetailViews.

Bug 2: createdAt and updatedAt never set on memo records

  apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts
  create() built a LocalMemo object without populating createdAt or
  updatedAt. The LocalMemo type declares both as required strings,
  but TypeScript didn't catch the omission because the store relied
  on a TS Type assertion / partial-shape pattern.

  The Dexie creating hook in apps/mana/apps/web/src/lib/data/database.ts
  only auto-stamps userId + __fieldTimestamps — it does NOT auto-stamp
  createdAt. Module stores are expected to set their own timestamps
  (consistent with the todo, calendar, contacts, notes stores etc.).

  So every memoro memo had `createdAt === undefined`, and the
  DetailView's `new Date(memo.createdAt ?? '').toLocaleDateString('de')`
  rendered as "Erstellt: Invalid Date" for every single memo.

  Fix: set both timestamps explicitly in create() before the Dexie
  add. Existing memos in the wild are still broken — they'd need a
  one-shot migration to backfill createdAt from the
  __fieldTimestamps map, but that's a bigger commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:22:01 +02:00
Till JS
fbb71f9366 feat(admin): replace mock dashboard stats with real /admin/stats endpoint
The /admin route in the unified Mana web app was rendering hardcoded
mock data (42 users, 156 successful logins, 3 failed) for every
admin who opened it. The previous code had a TODO comment to wire
up a real endpoint and the backend half had been waiting for the
frontend half ever since the consolidation landed.

Backend (mana-auth):
  Add GET /api/v1/admin/stats — admin-only, returns the seven counts
  the dashboard needs in a single response. Each count is its own
  Drizzle query against auth.users / auth.sessions / auth.login_
  attempts; they run in parallel via Promise.all so total latency is
  dominated by the round-trip to Postgres, not the per-query work.

  Stats:
    - totalUsers      → users where deleted_at IS NULL
    - newUsers7d      → users created in the last 7 days
    - newUsers30d     → users created in the last 30 days
    - activeSessions  → sessions where expires_at > now() AND not revoked
    - uniqueUsers24h  → distinct user_id from sessions with last_activity
                        in the last 24h (and not revoked)
    - loginSuccess7d  → login_attempts where successful=true, last 7d
    - loginFailed7d   → login_attempts where successful=false, last 7d

  Plus a generatedAt ISO timestamp so the client can show staleness
  if it ever caches the response.

Frontend (apps/mana/apps/web):
  - Add adminService.getStats() in the existing admin API service
    (sits next to getUsers / getUserData / deleteUserData; uses the
    same authenticated base-client and ApiResult envelope).
  - Replace the onMount mock-data block in admin/+page.svelte with
    a single adminService.getStats() call. Drop the local Stats
    interface in favor of the AdminStats type exported from the
    service.
  - Guard the Success Rate calculation against division by zero on
    fresh deployments — when there have been no login attempts in
    the last 7 days, render '—%' instead of NaN%.

Verification:
  - mana-auth type-check unchanged (baseline errors only)
  - mana-auth runtime tests still 19/19 passing
  - svelte-check on the two changed web files: zero errors

Closes item #12 in docs/REFACTORING_AUDIT_2026_04.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:20:18 +02:00
Till JS
7b25f3abfb refactor(mana/web): migrate module DetailViews + sub-views to theme tokens (P5)
15 files across 11 modules — the consumer-specific style overrides on
top of DetailViewShell, plus a few module-internal sub-views and pages
(SymbolDetailView, SymbolsView, ContactPage, PageEditBar, TodoPage,
CycleCalendar, SymptomManager).

All :global(.dark) duplicates removed (theme system handles light/dark
via .dark class on <html>) and the hand-rolled #374151/#9ca3af palette
+ indigo/violet/red/green brand accents replaced with hsl(var(--color-X)).

Brand colors that should NOT track theme primary stay literal:
- cycles brand pink (#ec4899) — menstrual cycle tracker accent
- dreams indigo accents and skilltree violet star colors → color-primary
  (these were arbitrary indigo brand accents, now they follow the variant)
- semantic income/success green → color-success
- semantic error/danger red → color-error
- favorite/star amber/gold → color-warning

Final P5 batch closing out the visual track consolidation. Combined with
earlier P5 commits (foundation shells, page-shell, picker, list views),
mana-web now has a single coherent theme-token convention across the
workbench surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:15:47 +02:00
Till JS
94d7dd4831 feat(mana/web): tier-gate workbench picker, openApps, and per-route navigation
Guests and under-tier users could see and use every module in the
workbench because tier-filtering only existed in @mana/shared-branding's
MANA_APPS list — never in the workbench app-registry that the picker
and the page-level routes actually consume. Three leaks closed:

──── 1. Workbench AppPagePicker ────

The picker was calling getAllApps() and only filtering by "already
open in this scene". Result: a guest opening "Add page" saw all 32
modules including founder-only ones like dreams, finance, memoro.

Fix: new getAccessibleApps(userTier) helper in app-registry/registry.ts
joins the workbench in-memory map with MANA_APPS by id, looks up
each app's requiredTier, and filters via hasAppAccess. Apps that
exist in the workbench registry but NOT in MANA_APPS (`automations`,
`playground`, the `inventar` ↔ `inventory` id mismatch) default to
visible — hiding them would silently break internal tools for
founders/devs.

AppPagePicker now takes a `userTier` prop and calls
getAccessibleApps(userTier) instead of getAllApps(). (app)/+page.svelte
threads authStore.user?.tier into it.

──── 2. openApps soft-filter ────

The default Home scene seeds [todo, calendar, notes] — `notes` is
founder-tier, so a brand-new guest device would still try to render
the notes view in their workbench tab strip even though they can't
actually use it. Same risk for any cross-device synced scene that
contains gated apps (e.g. founder logs in on a public-tier secondary
account).

Fix: (app)/+page.svelte derives `openApps` through a soft filter
(isAppAccessible) instead of using workbenchScenesStore.openApps
directly. The store keeps the full list — we don't destructively
delete on tier downgrades — so the tabs reappear when the user
upgrades or signs in. Internal-only apps (no MANA_APPS entry)
stay visible by the same default-visible rule.

──── 3. Per-route tier gate in (app)/+layout.svelte ────

The wrapping <AuthGate> in (app)/+layout.svelte:
  - only runs onMount, so it doesn't react to client-side navigation
  - skips the tier check entirely when !authStore.isAuthenticated
  - has no per-route requiredTier — it's set once on the outer wrapper

So a guest typing /dreams or /cycles in the URL bar slipped past
silently and rendered the gated module. Same for a public-tier user
clicking through to /finance.

Fix: reactive `routeBlocked` derivation in the (app) layout:
  - Extract first path segment from $page.url.pathname
  - Look it up in MANA_APPS by id
  - If found and user (or 'guest') doesn't satisfy requiredTier,
    render an inline tier-denied panel instead of {@render children()}

The panel mirrors AuthGate's tier-denied design (same locked icon +
tier comparison + "Zur Übersicht" / "Anmelden" buttons) but works
reactively for any subsequent navigation. Routes that don't map to
a MANA_APPS id (settings, profile, admin, help, observatory, …)
fall through with routeAppId=null and are never blocked.

──── New helpers in app-registry ────

  getAccessibleApps(userTier?) — filtered AppDescriptor[]
  isAppAccessible(appId, userTier?) — boolean for single-app lookup

Both treat `userTier === undefined | null` as 'guest' (the lowest
tier in @mana/shared-branding). Both default-visible for apps not
in MANA_APPS so the workbench-internal tools keep working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:08:49 +02:00
Till JS
237516749a refactor(mana/web): migrate foundation shells + workbench scenes to theme tokens (P5)
Sweep across the workbench-foundation components that wrap or chrome
every app surface in mana-web. All :global(.dark) duplicates removed
(theme system handles light/dark via .dark class on <html>) and the
hand-rolled #374151/#9ca3af palette + indigo/violet brand accents
replaced with hsl(var(--color-X)).

Files migrated:
  - DetailViewShell — the inline-edit detail card my P2 introduced.
    Had compound :global(.dark .detail-view .X) selectors duplicating
    every input/border for dark mode. Now single source.
  - PickerOverlay — the workbench app picker / page picker.
  - page-carousel/PageCarousel — drop-target hover (was hardcoded violet)
    becomes color-primary.
  - workbench/AppPage — drop-target hover (was hardcoded green) becomes
    color-success.
  - workbench/scenes/{ConfirmDialog,SceneRenameDialog,SceneTabs} —
    modal overlays. The black-50% backdrop stays literal (hsl(0 0% 0% / 0.5))
    since it's a deliberate semi-transparent overlay, not theme-aware.
    The .danger button background switches from #dc2626 to color-error.
  - voice/VoiceCaptureBar — primary indigo accent + red error state
    both follow theme tokens now.
  - links/LinkedItems — text + border tokens.

These are foundational — every app rendered in the workbench inherits
its chrome and detail view from these files, so any visual tweak now
propagates everywhere consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:02:54 +02:00
Till JS
034a07d166 chore(workspace): remove redundant nested lockfiles + workspace.yaml
Three pnpm artifacts that were either Pre-Consolidation leftovers or
unintentional drift:

  - apps/context/pnpm-lock.yaml + apps/context/pnpm-workspace.yaml
    apps/context used to be its own nested workspace declaring
    apps/* and packages/*. After consolidation only apps/context/
    apps/mobile remains, and the root pnpm-workspace.yaml already
    matches it via 'apps/*/apps/*'. The nested lockfile (242 KB)
    was a separate dependency graph drifting independently from
    the root.

  - services/mana-media/packages/client/pnpm-lock.yaml
    Anomalous lockfile in a workspace sub-package. The root
    workspace already covers services/*/packages/* — no reason
    for client/ to maintain its own resolution.

Verified after deletion:
  - pnpm install completes cleanly (~16s) and now resolves
    apps/context/apps/mobile from the root lockfile (pnpm list
    confirms the workspace registration)
  - apps/api type-check still 0 errors
  - mana-auth tests still 19/19 passing

Tracked as item #26 in docs/REFACTORING_AUDIT_2026_04.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:57:11 +02:00
Till JS
12be86b5e6 chore: remove abandoned per-product workspace artifacts
Three independent dead-code cleanups bundled together because they
all touch dev scripts in the root package.json:

1. games/voxelava/ + games/worldream/ — orphaned game stubs
   ~5886 LOC of Svelte components, route handlers, and types with
   no root package.json in either directory, no CI references, no
   docker-compose entry, no mana-apps registry presence. The
   matching root scripts dev:worldream:web + worldream:dev pointed
   to a @worldream/web filter that doesn't exist as a workspace
   member. games/arcade and games/whopixels remain untouched.

2. apps/memoro/* — clean stale @memoro/web references
   apps/memoro/apps/web/ was removed during the consolidation; the
   memoro frontend now lives in apps/mana/apps/web/src/lib/modules/
   memoro/. But several scripts still pointed at the deleted
   filter:
     - root: dev:memoro:web (deleted), dev:memoro:app + :full
       rewritten to drop the :web piece (server + audio-server
       only)
     - apps/memoro/package.json: dev:web removed, top-level dev
       script removed (filtered @memoro/* which would have hit
       the dead web filter)

3. apps/memoro/apps/server: declare @mana/notify-client dep
   src/lib/notify.ts:6 has been importing @mana/notify-client
   without declaring it in package.json — works by accident via
   hoisted node_modules in the workspace. Add the dep so the
   import is properly tracked. Found while verifying that
   notify-client (which has 0 declared consumers) was actually
   safe to keep.

Tracked as items #18, #19, #29 in
docs/REFACTORING_AUDIT_2026_04.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:56:51 +02:00
Till JS
b4dd646fd7 feat(memoro): auto-generate voice memo titles via the LLM task queue
First real-world consumer of the @mana/shared-llm tier framework.
After STT transcription completes for a voice memo, the memos store
fire-and-forgets a generateTitleTask into the persistent task queue
with refType:'memo' + refId:memoId. A module-side watcher subscribed
via Dexie liveQuery to completed task rows writes the result back
into memo.title and deletes the queue row to mark it consumed.

What this commit ships:

  apps/mana/apps/web/src/lib/llm-tasks/generate-title.ts
    - generateTitleTask: minTier='none', contentClass='personal'
    - runLlm: sends a German system prompt asking for a 3-7 word
      title, defensive cleanup of any quotes/markdown the model
      might leak through despite the prompt
    - runRules: takes the first sentence (split on .!?\n), caps
      at maxWords/60-chars, returns a non-empty fallback string.
      Predictable and free, works on every device including the
      ones where the user has opted out of all LLM tiers.

  apps/mana/apps/web/src/lib/llm-task-registry.ts
    - Register generateTitleTask alongside extractDate + summarize
      so the queue processor can resolve the name back to the
      task object after a row is pulled from the persistent table.

  apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts
    - After transcribeMemo successfully writes the transcript +
      processingStatus:'completed', enqueue a generateTitleTask
      tagged with refType:'memo' + refId + priority:1. Skips the
      enqueue if the memo already has a non-empty title (so
      manually-titled memos aren't overwritten on re-transcription)
      or if the transcript came back empty.
    - Wrapped in try/catch — queue failures must NEVER break the
      transcription happy path.

  apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts
    - startMemoroLlmWatcher() / stopMemoroLlmWatcher()
    - Subscribes via Dexie liveQuery to llmQueueDb.tasks rows
      where state='done', taskName='common.generateTitle',
      refType='memo'. For each row:
        1. Skip + delete row if result isn't a string (defensive)
        2. Skip + delete row if memo no longer exists (deleted
           between enqueue and result)
        3. Skip + delete row if memo already has a manual title
           (user typed one during the LLM round-trip)
        4. Otherwise: encryptRecord + memoTable.update with
           { title: result, updatedAt: now }, then delete the
           queue row to mark it consumed.
    - Module-scope subscription handle, idempotent start/stop.

  apps/mana/apps/web/src/routes/(app)/+layout.svelte
    - startMemoroLlmWatcher() in handleAuthReady's Phase A right
      after startLlmQueue(). The watcher needs to run regardless
      of whether the user is currently on /memoro — a memo
      transcribing in the background should auto-title even
      while the user is doing something else.
    - stopMemoroLlmWatcher() in onDestroy alongside stopLlmQueue().

End-to-end flow with a Tier 0 user (no AI enabled):

  1. User records a memo via voice capture
  2. memos.svelte.ts createWithTranscription() inserts the memo
     with processingStatus:'processing'
  3. transcribeMemo POSTs the audio to mana-stt, awaits the
     transcript
  4. Successful transcript → memos.svelte.ts writes
     { transcript, processingStatus:'completed' } to memoTable
  5. Same function enqueues generateTitleTask with the transcript
  6. LlmTaskQueue processor picks it up (the queue is running in
     the background since layout init), calls
     orchestrator.run(generateTitleTask, { text: transcript })
  7. Orchestrator: Tier 0 user → no LLM tier → falls through to
     runRules() which returns the first-sentence heuristic
  8. Queue marks the row done with the rules-tier title string
  9. Memoro watcher's liveQuery fires with the new completed row
  10. Watcher writes title + deletes the queue row
  11. ListView's existing useLiveQuery on memoTable picks up the
      title change automatically

End-to-end flow with a Browser-tier user:

  Steps 1-6 identical, then:
  7. Orchestrator: browser tier ready → calls
     generateTitleTask.runLlm with the BrowserBackend
  8. Web Worker (Phase 3) runs Gemma 4 E2B against a 32-token
     budget, returns a 3-7 word German title
  9-11. Same as Tier 0 — the title lands in memo.title without
        the user clicking anything

This is the validation the entire 4-phase architecture was built
for: a module-side auto-feature that's completely tier-agnostic,
fire-and-forget, persistent across reloads, and that gracefully
degrades from Gemma 4 down to a regex when the user has opted out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:55:26 +02:00
Till JS
7a0959e519 refactor(mana/web): migrate PageShell to theme tokens (P5)
The workbench paper card containing every app — was hardcoded cream
(#fffef5) light + dark brown (#252220) dark via :global(.dark).
Now uses hsl(var(--color-card)) so it follows the active theme variant.
The drag-handle bar, move buttons, window buttons, resize handle and
title all switch from hand-rolled gray scale to color-foreground /
color-muted-foreground / color-surface-hover. The close button hover
becomes color-error. The resize purple glow becomes color-primary.

This is the foundational shell — every app rendered in the workbench
inherits its background from this file, so the migration here unblocks
visual consistency across the whole app surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:53:35 +02:00
Till JS
0b9f85cfc5 refactor(mana/web): migrate dreams/ListView style to theme tokens (P5)
31 hand-rolled rules + their :global(.dark) duplicates → hsl(var(--color-X)).
The largest scoped-CSS file in the P5 sweep. Indigo accents (#6366f1) for
the inline-editor borders, status badges, view-tab active state, filter-tab
active state, search-input focus, and ins-symbol active state all become
hsl(var(--color-primary)) so they follow the active theme variant. Lucid
star ratings (was hardcoded amber) become hsl(var(--color-warning)).
Danger reds for transcription failure / delete become hsl(var(--color-error)).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:49:31 +02:00
Till JS
feb19d8a4c refactor(mana/web): migrate cycles/ListView style to theme tokens (P5)
11 hand-rolled rules + their :global(.dark) duplicates → hsl(var(--color-X)).
The brand pink (#ec4899) stays literal — it's the menstrual cycle tracker
brand color and should not track theme variants. Danger reds switch to
hsl(var(--color-error)) so they follow the theme palette.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:49:29 +02:00
Till JS
606b836f25 refactor(mana/web): migrate finance/ListView style to theme tokens (P5)
21 hand-rolled rules + their :global(.dark) duplicates → hsl(var(--color-X)).
income/expense semantic colors switch from literal #22c55e/#ef4444 to
hsl(var(--color-success))/hsl(var(--color-error)) — they keep their meaning
(green for money in, red for money out) but now follow the theme palette.
.add-btn primary action and .cat-chip.selected state move from hardcoded
indigo to color-primary so they follow the active theme variant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:06:10 +02:00
Till JS
01a10b2d87 refactor(mana/web): migrate notes/ListView style to theme tokens (P5)
22 hand-rolled rules + their :global(.dark) duplicates → hsl(var(--color-X)).
The inline-editor border/background (was hardcoded indigo rgba) now uses
hsl(var(--color-primary) / alpha). The .ed-btn.primary save button (was
hardcoded #6366f1) becomes hsl(var(--color-primary)) so it follows the
active theme variant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:05:13 +02:00
Till JS
d5440bbf98 refactor(mana/web): migrate todo/ListView style to theme tokens (P5)
18 hand-rolled rules + their :global(.dark) duplicates → hsl(var(--color-X)).
Checkbox checked-state (was hardcoded green-500) becomes color-success.
Drag-target hover (was hardcoded violet-500) becomes color-primary.
Tag-pill keeps its --tag-color custom property logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:04:27 +02:00
Till JS
d9cd92c4e5 refactor(mana/web): migrate contacts/ListView style to theme tokens (P5)
16 hand-rolled rules + their :global(.dark) duplicates → hsl(var(--color-X)).
The drag-target hover (was hardcoded green-500 rgba) becomes color-success.
The favorite-star (was hardcoded amber-500) becomes color-warning. Tag-pill
keeps its --tag-color custom property logic (per-tag user color).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:03:43 +02:00