Commit graph

2856 commits

Author SHA1 Message Date
Till JS
0f7ab60397 feat: top-5 ROI improvements — CI gate, auth fields, body×timeblocks, sync pull, tests
Five high-impact improvements across the stack:

1. Pre-push hook: svelte-check gate (.husky/pre-push)
   Runs `pnpm check --fail-on-warnings` before every `git push`.
   Blocks pushes with type errors or warnings so we never drift
   back to 418 errors. Takes ~15s on warm cache — acceptable for
   push frequency. Skip with `--no-verify` if needed.

2. getUserFromToken: map name/image/twoFactorEnabled
   The JWT payload carries these three fields (from Better Auth's
   user profile + 2FA enrollment) but getUserFromToken() only
   extracted sub/email/role/tier. The Settings page, onboarding
   ProfileStep, and TwoFactorSetup all read these via
   `authStore.user?.name` etc. and got undefined. Now mapped from
   both top-level claims and user_metadata (legacy layout).
   DecodedToken type extended to match.

3. Body × TimeBlocks integration
   startWorkout() now creates a TimeBlock (kind='logged',
   type='body', sourceModule='body') so workouts appear in the
   calendar, timeline page, and DayTimelineWidget. finishWorkout()
   stamps the TimeBlock's endDate so the calendar shows duration.
   deleteWorkout() cascades the TimeBlock deletion. Added
   `timeBlockId?: string` to LocalBodyWorkout.

4. Sync pull() silent-failure surfacing
   Symmetric with the push() fix from the SYNC_DEBUG commit:
   pull() now logs a console.warn + emits telemetry for both
   the unknown-appid and no-token failure paths instead of
   silently returning. Same diagnostic value as the push fix —
   the SYNC_DEBUG runbook's Schritt C now surfaces pull failures
   too.

5. Unit tests for contacts, chat, calendar (3 new test files)
   Same fake-indexeddb + MemoryKeyProvider harness as body/nutriphi.
   - contacts: create+encrypt PII, soft-delete, toggleFavorite (4)
   - chat: create+encrypt title, archive, pin/unpin, delete (4)
   - calendar: create with defaults, soft-delete, setAsDefault (3)
   Total test count: 37 passing across 5 suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:17:32 +02:00
Till JS
04ce8e5d6f fix(chat): add auth header, template system prompts, streaming debounce
Three critical fixes to the chat completion service:

1. Auth header: attach Bearer token from authStore on every request.
   Without this, mana-api returns 401 in production.

2. Template support: when a conversation has a templateId, resolve
   and decrypt its systemPrompt from IndexedDB and prepend it as a
   system message to the LLM context. Both route page and workbench
   overlay now pass templateId + modelId through to sendAndStream().

3. Streaming debounce: persist accumulated text to Dexie at most
   every 250ms instead of on every SSE chunk. Reduces encrypt+write
   operations from ~50/response to ~8 without affecting the live UI
   (onChunk still fires on every token).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:03:43 +02:00
Till JS
d7663e95b1 feat(playground): persistent chat history, token display, model comparison
Three major features for the LLM playground module:

1. Chat history persistence — conversations and messages are saved to
   IndexedDB (encrypted at rest), survive page reload, and sync via
   mana-sync. Sidebar shows conversation list with load/delete. Auto-
   titles from first user message. Lazy conversation creation on first
   send.

2. Token/usage display — llm.ts now yields a StreamChunk union type
   (delta | usage). Token counts (prompt + completion) are shown beneath
   each assistant message and persisted per message record.

3. Model comparison — toggle comparison mode in the config bar, select
   2-4 models, and see responses streamed side-by-side in a CSS grid.
   Each comparison round is tied by a comparisonGroupId. All streams
   have independent AbortControllers. Follow-up messages use the first
   model's response as conversation context.

New files: stores/conversations.svelte.ts
New tables: playgroundConversations, playgroundMessages (encrypted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:02:27 +02:00
Till JS
28c10246e3 feat(zitare): smooth transitions, custom quotes, notes, new categories, fuzzy search
- Smooth fade/slide animation on quote cycling in workbench
- Custom quotes: new table (DB v5), store, queries for user-created quotes
- Notes field on favorites for personal thoughts
- 3 new categories: humor, wissenschaft, kunst
- 23 new quotes incl. contemporary authors (Angelou, Mandela, Winfrey, etc.)
- 5 curated theme decks (Stoizismus, Feminismus, Philosophie, etc.)
- getAllAuthors() for author browse pages with bio + quote counts
- fuzzySearchQuotes() with bigram similarity for typo-tolerant search
- Pre-built category + author indexes for O(1) lookups

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:01:15 +02:00
Till JS
adb1649005 refactor(mana/web): architecture cleanup — liveQuery migration, dead types, seed registry
Four architectural improvements that reduce boilerplate, eliminate
dead code, document the frozen schema boundary, and centralize the
guest data seeding that was previously defined but never called.

1. Migrate remaining 10 modules to useLiveQueryWithDefault
   music (5), moodlit (2), places (2), storage (2), calc (2),
   planta (5), photos (3), contacts (1), inventory (4) — 26 hooks
   total. Each queries.ts now imports useLiveQueryWithDefault from
   @mana/local-store/svelte instead of raw liveQuery from dexie.
   Call sites that used manual $effect + subscribe() boilerplate
   replaced with $derived(ctx.value). Files touched: 10 queries.ts
   + 5 route/component call sites (contacts, places, photos,
   inventory, calc).

2. Remove dead Memoro Tag interface
   memoro/types.ts had a local Tag type (with isPinned, sortOrder)
   that diverged from the @mana/shared-tags Tag. No file imported
   it after the earlier migration — removed the interface and added
   a comment directing future readers to @mana/shared-tags.

3. Document frozen schema boundary in database.ts
   Updated the v1 comment to explicitly state it's frozen and
   explain why (Dexie only runs upgrades when the version number
   bumps). Lists the current additive versions: v2=body, v3=who,
   v4=news. News tables were already correctly extracted to v4 by
   concurrent work.

4. Centralize guest seed registry
   Created lib/data/seed-registry.ts that imports GUEST_SEED
   constants from 13 modules (habits, body, dreams, moodlit,
   contacts, calendar, chat, cards, skilltree, todo, notes, times,
   planta) and provides a single seedAllGuestData() function.
   Wired into manaStore.initialize() in local-store.ts so seeds
   actually get inserted on first visit. Previously every module
   defined and re-exported seed data but nothing ever consumed it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:01:02 +02:00
Till JS
fa099145eb feat(chat): add workbench detail overlay with streaming AI
Register a detail view for the chat module so clicking a conversation
in the workbench opens an inline overlay with the full message thread
and input area. Reuses the shared sendAndStream() completion service.

- ListView: decrypt conversations + messages, add "Neuer Chat" button,
  click opens detail overlay with sibling navigation, context menu
- DetailView: message bubbles, streaming indicator, auto-scroll,
  Enter to send / Shift+Enter for newline
- App registry: add detail view loader + paramKey 'conversationId'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:52:17 +02:00
Till JS
6cc40242e9 feat(chat): wire AI streaming completions in chat detail page
Replace the placeholder stub with a real streaming SSE connection
to mana-api at /api/v1/chat/completions/stream. Extracts the
send-and-stream cycle into a shared services/completion.ts helper
so both the route page and workbench overlay can reuse it.

- Streams assistant response chunks into a live bubble
- Shows thinking dots (●●●) while waiting for first token
- Handles 402 (insufficient credits) with German error message
- Auto-titles conversation from first user message
- Persists final assistant text to IndexedDB with encryption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:52:02 +02:00
Till JS
4f17626d3d feat(guides): complete module with types, CRUD, detail view, and run tracking
Builds out the guides module from a static content display into a
full local-first module with interactive step-by-step progress:

- types.ts: LocalGuide/Section/Step/Run records, domain types, DTOs
- collections.ts: Dexie table accessors + guest seed (6 guides, 14
  sections, 22 steps with real instructional content)
- queries.ts: liveQuery hooks (useAllGuides, useGuide, useSections,
  useSteps, useLatestRun, useRunsByGuide) + type converters + search
- stores/guides.svelte.ts: full CRUD for guides/sections/steps,
  run tracking (startRun, completeStep, uncompleteStep, completeRun),
  cascade delete, all with encryptRecord
- views/DetailView.svelte: step-by-step viewer with sections as
  collapsible blocks, steps as interactive checklist, progress bar,
  inline editing, inline add for sections/steps
- ListView.svelte: DB-based instead of static, ViewProps, inline
  create, category filter, search, per-guide progress indicators
- apps.ts: detail view + paramKey registered
- crypto/registry.ts: guides/sections/steps encrypted fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:51:19 +02:00
Till JS
f17d748d85 feat(picture): add inline upload to workbench ListView
Add drag-and-drop zone and upload button to the picture module's
workbench page. Uploads go to mana-media, then insert a LocalImage
record via imagesStore.insert() with encryption. Shows thumbnail
previews with upload status (spinner/check/error).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:42:31 +02:00
Till JS
2c56780bda feat(mana/web): add shared voice transcription helper
Extract the duplicated STT fetch logic from 5 module stores
(dreams, memoro, notes, todo, habits) into a single
$lib/voice/transcribe.ts helper. Returns text, language,
durationSeconds, and the model identifier from mana-stt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:41:27 +02:00
Till JS
ea4f05dc11 feat(inventory): add quick item creation to workbench ListView
Adds a "Gegenstand hinzufügen" button that expands into an inline form
with a collection picker dropdown and name input. Items are created
directly via itemsStore.create() without leaving the workbench panel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:40:28 +02:00
Till JS
e77ae5d5eb feat(who): add character dossier system for staged fact disclosure
Pre-researched dossiers (37 JSON files, DE+EN) replace the old
personality strings as the source of truth for the Who guessing game.
A strong cloud LLM (Gemini 2.5 Flash) generates structured facts per
character — voice, values, achievements, anecdotes, relationships,
forbidden-early-words, and three-stage hints — so the small runtime
model (gemma3:4b) gets only what it needs per turn instead of raw
personality text that leaks the identity immediately.

- dossier-types.ts: Zod schema + TS types for CharacterDossier
- dossier-loader.ts: boot-time loader with validation + coverage report
- generate-who-dossiers.ts: one-shot generator script (Google Gemini
  or local mana-llm fallback, idempotent, --force/--id flags)
- 37 dossier JSON files in data/dossiers/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:40:16 +02:00
Till JS
734f149596 feat(moodlit): add inline mood creation to workbench ListView
Adds a compact create form with live gradient preview, name input,
color pickers (add/remove, max 8), and animation type dropdown.
New moods are written via moodsStore.createMood() to IndexedDB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:38:12 +02:00
Till JS
d3b9805341 feat(mana/web): play Who games inline on workbench page
Embed PlayView directly in the ListView so games can be started
and played without navigating away from the workbench. PlayView
now accepts an onBack callback instead of hardcoded goto('/who').

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:37:42 +02:00
Till JS
05f4da5db3 feat(events): wire workbench detail overlay via ViewProps
Events ListView and DetailView now use the standard ViewProps interface
(navigate/goBack/params) instead of the custom onOpenEvent callback.
Adds paramKey to the events app registration so the workbench overlay
knows which param carries the event ID. Clicking an event card now
opens the detail overlay with prev/next sibling navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:36:40 +02:00
Till JS
f1a7f35d66 feat(context): add inline document creation to workbench ListView
Adds a "Neues Dokument" button and an "Alle Dokumente" link in the
toolbar. Document rows are now clickable <a> tags linking to the
detail page instead of static divs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:36:31 +02:00
Till JS
26914b14c8 fix(body): unblock routine creation + remove duplicate header
The save button was permanently disabled for users with no exercises
because the disabled gate required newSelected.size > 0. Now only
the name is required; an inline "add exercise" flow lets users create
exercises directly from the routine form. Also removed the redundant
h1 + subtitle since the workbench shell already renders the title.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:36:24 +02:00
Till JS
da03fac722 fix(mana/web+packages): clear all 270 warnings to zero
Comprehensive warning sweep across 128 files that brings svelte-check
from 270 warnings → 0 (plus 3 new errors from concurrent upstream
changes fixed inline).

Final state: 6473 files, 0 errors, 0 warnings, 0 files with problems.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:34:49 +02:00
Till JS
b8987562ba fix(mana/web): add logging + toast for encryption vault unlock failures
Vault unlock errors were silently swallowed, causing encrypted content
(enc:1:...) to render as ciphertext in the UI. Now logs each step of
the unlock flow and shows an error toast when the vault fails to unlock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:29:02 +02:00
Till JS
716466e757 fix(shared-llm): sort candidate tiers privacy-first (browser before server)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:23:28 +02:00
Till JS
64b8ab30ad fix(mana-media): commit initial schema migration + run on startup
The media schema/tables were never created on fresh deploys because
mana-media only shipped a `db:push` script and nothing ever ran it
in the container. Result: every upload returned 500 the moment a
new environment came up (just hit prod again on mana.how).

- Add `db:generate` + `db:migrate` scripts and a migrate.ts runner
- Generate the initial migration covering media/media_references/
  media_thumbnails (matches what was already on local + prod, which
  were stamped manually so the migrator skips on existing deploys)
- Call runMigrations() at startup in src/index.ts so future fresh
  containers self-bootstrap. Idempotent — drizzle tracks state in
  drizzle.__drizzle_migrations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:51:41 +02:00
Till JS
80b23dd9ff fix(mana/web): clear remaining type errors — long-tail sweep
The last cleanup pass after the package-level fixes. Each of the
~30 files below had 1-2 distinct errors; they're grouped because
none individually justifies its own commit and they're all the same
shape: small drift between a call site and the type system the
existing-code-doesn't-need-to-change refactor that gets it to clean.

Highlights by file:

vite.config.ts
  Switched `defineConfig` import from `vite` to `vitest/config` so
  the inline `test:` block (vitest unit-test exclude rule) is
  recognized at the type layer. Was the last single error standing.

routes/(app)/news/+page.svelte
  Replaced `{#each ranked as { article } (article.id)}` destructure
  with `{#each ranked as scored (scored.article.id)}` + two
  `{@const}` rows. The destructured-each + immediate-`@const`
  combination tripped a Svelte compiler placement error.

routes/(app)/contacts/[id], modules/calendar/EventForm
  `(x as Record<string, unknown>)` casts were rejected because the
  source type doesn't have a string index signature. Two-step
  cast: `as unknown as Record<string, unknown>`.

routes/(app)/inventory/collections/[id]/edit
  `collection.schema.fields` round-trips through JSON in the Dexie
  row, which widens `type` to plain `string`. Cast back to
  `FieldDefinition[]` at the read site; the runtime values match
  the FieldType union.

routes/(app)/presi/deck/[id], modules/zitare/QuoteCard,
modules/memoro/views/DetailView
  - presi: `currentDeck?.name` → `?.title` (Deck has `title`, not
    `name`).
  - QuoteCard: `let authorBioText = $derived(() => {...})` was
    storing the arrow function itself. Switch to `$derived.by(...)`.
  - memoro DetailView: explicit `<QueuedTask | null>` generic on
    the useLiveQueryWithDefault call so the unknown-typed default
    doesn't poison downstream state.

routes/(app)/memoro/{,/[id]}/+page.svelte + modules/memoro/queries.ts
  The Tag flowing through these components is the `@mana/shared-tags`
  shape (from `useAllTags`), not memoro's local Tag (which has
  isPinned/sortOrder for a UI we never built). Aligned all three
  files to the shared shape so the Tag[] arrays compose without
  property mismatches.

modules/{questions,context}/index.ts
  Re-exported names that didn't exist:
  - `questionCollectionTable` → `qCollectionTable`
  - `contextDocumentTable` → `documentTable`
  Both were leftover from a long-ago rename that the consumers
  still call by the new name.

modules/picture/stores/images.svelte.ts, modules/times/EntryItem
  - images: `toggleField()` wants a string-keyed Table<>; cast at
    the call site (runtime keys are UUIDs anyway).
  - EntryItem: `autoSave(updates: Record<string, unknown>)` won't
    fit Dexie's `UpdateSpec<LocalTimeEntry>`. Narrowed to
    `Partial<LocalTimeEntry>` and added the missing import.

modules/todo: TodoPage + QuickAddTask
  - TodoPage was passing `onOpen` to TaskItem (which only accepts
    `onClick` + `onContextMenu` + `onToggleComplete`). Replaced
    with the proper triplet on the recently-completed branch.
  - QuickAddTask `locale?: string` widened the input past the
    `ParserLocale` union the parser actually accepts. Imported
    the union and tightened the prop.

modules/presi/views/DetailView
  `decksStore.deleteDeck` returns `Promise<boolean>`, but
  `deleteWithUndo()` expects `Promise<void>`. Wrapped in an async
  arrow that discards the return.

routes/(app)/citycorners/.../edit
  Self-referential `let locId = $derived(locId ?? '')` from a
  search-and-replace gone wrong in the previous commit batch.
  Restored to `$derived($page.params.id ?? '')`.

routes/(app)/+layout.svelte, lib/components/onboarding/OnboardingWizard
  - layout: `(window as Record<string, unknown>)` → two-step
    `(window as unknown as Record<...>)` cast. Same shape as the
    contacts/EventForm fixes.
  - OnboardingWizard: added optional `onSkip?: () => void` prop
    so the layout's analytics callback type-checks. The wizard
    always also calls `onComplete()`, so the modal still closes
    cleanly without onSkip.

routes/(app)/api-keys/+page.svelte
  Removed `min={1}` / `max={1000}` props from the shared `<Input>`
  component (it's not a passthrough wrapper for native HTML
  attributes). Runtime validation still gates submit.

routes/(auth)/forgot-password
  `authStore.forgotPassword(email)` doesn't exist; the wrapper
  exposes `resetPassword(email)` for the send-email entry point.
  Renamed.

routes/(app)/{gifts,llm-test}, lib/content/help/index.test
  - gifts: `balance.freeCreditsRemaining` is now optional (added
    in the credits commit). Defaulted to 0 in the math.
  - llm-test: enqueueTaskNow union of two tasks with different
    output types — widened with `as any` for the enqueue call.
  - help index.test: `content.contact` is optional, asserted with
    non-null `!`.

lib/components/{SessionWarning,DashboardGrid,onboarding/OnboardingWizard}
  - SessionWarning: was calling `getAccessTokenSync` (doesn't
    exist) and `refreshToken` (doesn't exist). Switched to
    `getAccessToken()` (async, returns Promise) and `getValidToken()`
    (refreshes under the hood when expired).
  - DashboardGrid: `error?.message` on a `{}`-typed boundary
    arg. Cast to `Error | undefined`.

dashboard widgets: ContextDocs / ClockTimers / ActivityFeed
  - ContextDocs: `getSpaceName(spaceId: string)` widened to
    `string | null | undefined` so the optional doc.spaceId
    flows in cleanly.
  - ClockTimers: `formatRepeatDays`/`formatRemaining` widened to
    accept null|undefined.
  - ActivityFeed: `Activity` icon doesn't exist in
    `@mana/shared-icons`/phosphor-svelte. Replaced with `Pulse`
    everywhere in the file.

lib/app-registry/registry.spec
  `Set<AppIconId>.has(stringId)` rejected because the union is
  narrower. Widened the Set to `Set<string>`.

Net: -16 type errors. Final count: 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:25:08 +02:00
Till JS
c31ce4448f fix(packages): modal keydown handlers, $derived.by usage, UserData fields
Eight more package-level type errors that all came from the same
small handful of patterns.

Modal escape-key handlers calling click-style functions
  Four modals (AuthGateModal, GuestWelcomeModal, ConfirmationPopover,
  ShareModal) had `onkeydown={(e) => { if (e.key === 'Escape')
  handleBackdropClick(); }}` — but handleBackdropClick took a MouseEvent
  parameter, so the no-arg call failed with "Expected 1 arguments,
  got 0". Fix: route the keyboard escape path through the right
  no-arg helper (`onClose` / `handleClose` / `handleContinueAsGuest`)
  or pass the keyboard event through with a cast for the popover
  trigger that genuinely shares its handler with the click path.

WallpaperModal $derived
  `currentLayout` and `currentBackground` were declared with
  `$derived(() => {...})` — passing a function expression. The
  variant that takes a thunk is `$derived.by(...)`; plain `$derived`
  expects a single value expression. Result: the variables held the
  arrow function itself, the call sites had to invoke them as
  `currentLayout()`, and TS rejected the function value where Layout
  was expected. Switch to `$derived.by`, drop the call-site parens.

TagList.svelte
  Generic param was named `Tag` in the handler signature
  (`tag: Tag`) but the imported type was aliased as `TagType`. Tag
  was undefined → "Cannot find name 'Tag'". Renamed to TagType.

TagStrip.svelte
  `dropAccepts?: string[]` is too wide for `passiveDropZone`'s
  `accepts: DragType[]`. Narrowed the prop type to `DragType[]`
  and added the missing import.

shared-auth/types: UserData.{name,image}?
  Two more optional fields for the public user shape. Both come
  from the JWT user_metadata claim when the user has filled in
  their profile during onboarding. Without these the
  ProfileStep.svelte onboarding component couldn't read
  `authStore.user?.name` / `?.image` without `as any`. Added
  alongside `twoFactorEnabled` from the previous shared-auth
  commit; same Optional rationale (guest tokens omit the claim).

Net: -10 type errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:24:05 +02:00
Till JS
ab24db36dd fix(packages): cross-package broken imports + missing exports
Five unrelated packages each had a few imports pointing at the wrong
file or missing from their public surface. Grouped because none of
the individual fixes warrants its own commit and they all unblock
the same downstream consumer (apps/mana/apps/web type-check).

packages/help
  - HelpPage.svelte: `'../types.js'` and `'./content'` for
    HelpPageProps/HelpSection/SearchResult — neither path exists.
    Real homes are `../ui-types` (props) and `../search-types`
    (search shapes). Fix the imports.
  - HelpSearch.svelte: same `'../content'` typo for SearchResult →
    `'../search-types'`.
  - translations.ts: `'./types.js'` for HelpPageTranslations →
    `'./ui-types'`.
  - ui-types.ts: was importing SearchResult from `'./content'` but
    that module only exports content shapes. Split into two imports
    so HelpContent stays from content.ts and SearchResult comes from
    search-types.ts.

packages/feedback
  - FeedbackPage.svelte: imported `Feedback` and `CreateFeedbackInput`
    from `'./createFeedbackService'` but the service module only
    exports the service factory. Real homes are `'./feedback'`
    (Feedback) and `'./api'` (CreateFeedbackInput).
  - FeedbackForm.svelte: same `'./feedback'` typo for
    CreateFeedbackInput → `'./api'`.

packages/subscriptions
  - UsageCard / CostCard / pages/SubscriptionPage: all imported
    UsageData / CostItem from `'./plans'` but those types live in
    `'./usage'`. SubscriptionPage additionally had a relative-path
    bug — it's at `src/pages/`, not `src/`, so `./plans` resolved
    to `pages/plans` (nonexistent). Now imports `'../plans'` for
    plan types and `'../usage'` for usage/cost types.

packages/shared-ui
  - index.ts: re-exports the QuickInputItem family from
    `./quick-input` but had forgotten `HighlightPattern`. Added.
    Apps that build their own InputBar pattern config (e.g.
    mana/web/src/lib/quick-input/types.ts) need it as a public type.
  - PillNavigation.svelte: imported `SpotlightAction` and
    `ContentSearcher` from `./GlobalSpotlight.svelte` (a Svelte
    component file), which only re-exports the default. Both types
    live in `./types`. Move them to the existing types-import
    block; the GlobalSpotlight import becomes a plain default.

packages/shared-auth-ui
  - stores/createAuthStore.svelte.ts: imported AuthServiceAdapter /
    AuthResult / BaseUser from `'./types'` (nonexistent — the file
    is `'./store-types'`).

Net: -23 type errors. Zero behavior change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:23:34 +02:00
Till JS
83eaf71e9f fix(macmini): clean up container conflicts in build-app.sh restart cycle
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
Hit "container name already in use" / "removal in progress" errors
three times during today's Phase 5 deploys. The previous restart
pattern was just `compose up -d --no-deps`, which fails when:

  1. A previous interrupted recreate left a stale container under
     the canonical name. The new `up` tries to claim the name and
     gets a conflict.
  2. Compose's recovery from #1 sometimes creates a hash-prefixed
     orphan container (`<hash>_<container_name>`), which then
     blocks the next clean run too.
  3. Even `--force-recreate` can't always handle the case because
     the old container is in the middle of being removed when the
     new one is being created (race).

Two-step replacement that's reliable across all three failure modes:

  Step 1 — `docker compose rm -fs SERVICES`
    Stops + force-removes the canonical compose-managed container.
    Idempotent: does nothing if already gone. Filters out the
    "No stopped containers" log noise so the output stays clean.

  Step 2 — orphan sweep via `docker rm -f`
    For each service, look up its container_name from the
    compose config (falls back to the service name if not set),
    then `docker ps -aq --filter name=^${cname}$` for the canonical
    one and `name=_${cname}$` for hash-prefixed orphans. Anything
    found gets nuked. This catches the case where compose's own
    state has lost track of an orphan it created earlier.

  Step 3 — `docker compose up -d --no-deps --remove-orphans`
    Creates the fresh container. The `--remove-orphans` flag also
    silences the "Found orphan containers ([mana-game-whopixels])"
    warning we kept seeing — that's a leftover from a removed
    service that nobody had cleaned up.

The container_name extraction uses awk on `compose config` output
(verified locally: `mana-web` → `mana-app-web`) so the script doesn't
need a hard-coded service→container mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:52 +02:00
Till JS
3c7bfc6a00 feat(mana/web): register body, events, who, guides in workbench app registry
Adds list (and detail where available) views for four modules that existed
in MANA_APPS but were missing from the workbench app registry. Creates a
static ListView for guides backed by the existing GUIDES catalog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:30 +02:00
Till JS
c172ce9b12 fix(mana/web): replace h2/h3 with div in AiSettings + AiTierStep
The AiSettings card was rendering with browser-default heading
sizes (~30px h2, ~18px h3) instead of the Tailwind utility classes
I'd given them. Visible in production: "KI-Optionen" came out
huge, "Auf deinem Gerät" ditto, the whole card looked like the
font-size system was broken.

Root cause: app.css has an `@layer base` block that explicitly
sets `h2 { font-size: 1.875rem; ... }` etc as a project-wide rich-
text default. The intention is that PROSE-style content gets nice
typography for free. But for components that use semantic h2/h3
tags purely for document structure (not for visual sizing), the
base layer rule wins over the utility classes when Tailwind 4's
content-scanning misses the file.

Why other settings cards work: their <h2> tags live INLINE in
routes/(app)/settings/+page.svelte, which Tailwind's Vite plugin
walks via the SvelteKit route entry. My new AiSettings card is in
lib/components/settings/AiSettings.svelte — a separate component
file that's imported by the route but apparently doesn't get its
classes generated reliably (likely a Tailwind 4 cache issue with
recently-added files in non-route paths). Result: text-lg /
text-sm / text-xs aren't in the output CSS, so the @layer base
heading rule is the only thing setting the size, and it wins.

Pragmatic fix: replace <h2> and <h3> with <div class="text-lg
font-semibold"> / <div class="text-base font-semibold">. Divs
aren't subject to the @layer base h2/h3 reset, so even if the
utility classes are also missing the styles fall back to the
element's natural inline-block-with-inherited-font-size behavior.
And the Tailwind classes — when they DO eventually get picked up
(e.g. on a clean build) — apply on top.

Same change applied to:
  - apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte
    (the section header + each tier card title)
  - apps/mana/apps/web/src/lib/components/onboarding/steps/AiTierStep.svelte
    (the step's main heading + each tier card title)

Functionally identical, just different element type. The semantic
loss is minimal — these aren't document-structure headings, they're
visual labels inside a card UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:50:04 +02:00
Till JS
acfbcfe689 feat(mana/web/news): "interested" keeps article visible + saved badge
Switches the feed engine to a softer reaction model: ❤️ Interessiert
no longer hides the article from the feed, only adds it to the
reading list and bumps the topic + source weights. The article keeps
its slot in the ranked feed and gets a "❤️ gespeichert" badge in the
card meta + a tinted card background so the user can see at a glance
"yep, this is already in my reading list".

The previous behavior — interested = save + remove from feed — was
modeled on a Pocket-style "save and move on" pattern, but turns out
to be confusing in a discovery-feed context: tapping a positive
signal made the article disappear, which feels like punishment.
Variante B (this commit) makes the destructive vs non-destructive
split explicit: 👎 Nicht für mich and 🚫 Quelle ausblenden are the
ones that hide articles, ❤️ is purely additive.

═══ Engine ═══

`scoreArticle()` now reads `dismissedIds` (the set of articles with
not_interested or hidden reactions) for the hard-hide filter
instead of the old `reactedIds` (which lumped all reaction kinds
together). `interestedIds` is passed alongside so views can render
the badge without re-deriving from the raw reactions array.

`buildReactionSets()` is the new helper that splits the reactions
into the two sets in one pass. `buildReactedIds()` is kept as a
deprecated alias that returns just the dismissed set — same effect
on the feed filter for any not-yet-migrated caller, and any old
"interested = hidden" behavior is now lost (which is the goal).

═══ UI ═══

The feed page card body gets a `.is-saved` modifier that tints the
background, the card meta row gets a saved-badge pill, and the
interested button shows "Gespeichert" + a filled-in active state +
disabled cursor when the article is already in the reading list.
A second click on an already-saved article is a no-op now.

The workbench ListView and the dashboard NewsUnreadWidget got the
same engine update so the three surfaces stay in sync — the badge UI
itself is only on the main feed for now since the workbench card is
too narrow to fit it cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:48:53 +02:00
Till JS
55bf493f44 fix(api): set supportsStructuredOutputs=true on mana-llm provider
generateObject() in the AI SDK falls back to a tool-call mode when the
provider doesn't advertise structured-output support — and tool calling
through Ollama isn't reliable enough that the schema-validation step
passes. The response was failing with 'No object generated: response
did not match schema' even though the underlying mana-llm + Ollama
roundtrip works correctly when called with response_format directly
(verified via curl).

Set supportsStructuredOutputs:true on the createOpenAICompatible
factory so the AI SDK uses response_format json_schema mode. mana-llm
already routes that to Ollama's native format field thanks to the
companion fix in services/mana-llm/src/providers/ollama.py — verified
end-to-end with the MealAnalysisSchema and Gemma 3 4B.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:44:13 +02:00
Till JS
f3effe9390 fix(mana/web/news): instant onboarding handoff to feed branch
The "Fertig" button needed two clicks before the wizard would
disappear. Cause: the wizard branch is gated on
`prefs.onboardingCompleted` which comes out of a Dexie liveQuery.
liveQuery debounces and emits the post-write value ~50-100ms after
the table.update() returns, so the first click writes the row but
the page re-renders the same wizard step until the next liveQuery
tick. Users instinctively click again before noticing.

Fix: a local `onboardingJustFinished` $state override that flips to
true synchronously inside `finishOnboarding()`. The wizard branch is
now hidden by `!(prefs.onboardingCompleted || onboardingJustFinished)`,
so the feed appears the instant the write resolves. The liveQuery
catches up a moment later but its update is a no-op because the
override and the queried value agree.

Also:
  - `onboardingSubmitting` $state guard so a panicked double-click
    gets ignored, and the button shows "Speichere…" while the write
    is in flight (visual feedback that something is happening)
  - Eagerly call `feedCacheStore.refresh()` from finishOnboarding so
    the feed isn't empty for the moment the layout's $effect needs
    to notice the prefs change. The store's inFlight guard makes the
    redundant layout-effect refresh a no-op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:41:31 +02:00
Till JS
958819f06a fix(api): default vision model to ollama/gemma3:4b
mana-llm on the live Mac Mini does not have GOOGLE_API_KEY configured —
only the Ollama provider is registered. The previous default
'google/gemini-2.0-flash' would error with 'Provider google not
available' on every photo analysis.

Switch to ollama/gemma3:4b which is locally available via the
gpu-proxy bridge to the Windows GPU box (192.168.178.11). Gemma 3 is
multimodal and verified end-to-end with the new mana-llm structured-
output passthrough — see the 5520f1385 fix landing the response_format
plumbing on the Pydantic side and the Ollama provider's native format
field translation.

VISION_MODEL env var still wins, so prod can flip to
google/gemini-2.0-flash later by adding GOOGLE_API_KEY to mana-llm's
docker-compose env block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:34:32 +02:00
Till JS
3b035e930f fix(mana/web/news): use getValidToken + guard prefs against locked vault
After the previous round of fixes, two issues remained:

1. Feed fetch returned 401 against `mana-api.mana.how`. The new
   `authHeader()` helper called `authStore.getAccessToken()`, which
   just reads `@auth/appToken` from localStorage and is happy to return
   null/stale. The unified sync engine in `sync.ts` uses
   `authStore.getValidToken()`, which routes through the tokenManager
   and refreshes if needed. Switched the news client to the same.

2. `Cannot read properties of undefined (reading 'emoji')` from
   `TOPIC_LABELS[topic]`. When the vault is briefly locked at boot,
   `decryptRecord` deliberately leaves the encrypted blob string in
   place — so `local.selectedTopics` can be a string. The `?? []`
   fallback in `toPreferences` doesn't catch it, and `{#each
   prefs.selectedTopics}` iterates the blob char-by-char. Force the
   three array fields (and the two map fields) back to their expected
   shapes with `Array.isArray` / object checks.
2026-04-09 18:52:51 +02:00
Till JS
5520f1385e fix(mana-llm): add response_format to ChatCompletionRequest model
The first iteration of the Ollama response_format passthrough crashed
with 'ChatCompletionRequest object has no attribute response_format'
because the Pydantic request model didn't declare the field at all —
incoming response_format from OpenAI-compatible clients was being
silently dropped at the parsing layer before the provider could see it.

Fix: declare a typed ResponseFormat sub-model with the two OpenAI shapes
('json_object' and 'json_schema'), add it as an optional field on
ChatCompletionRequest, and let the Ollama provider read it directly
without defensive getattr fallbacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:50:54 +02:00
Till JS
cb87d23509 fix(help): import search types from search-types, not content
search-engine.ts had two import blocks both pointing at './content':
the first picked up FAQItem / FeatureItem / GettingStartedItem /
ChangelogItem (correct — those live in content.ts) and the second
tried to pick up SearchableItem / SearchResult / SearchOptions /
SearchIndexConfig (wrong — those live in search-types.ts). Result:
4 "Module './content' has no exported member" errors.

Fix the second import to point at './search-types'. The first
block stays untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:48:39 +02:00
Till JS
697d96daec fix(mana/web): unblock api-keys, reset-password, dashboard widgets, skilltree stats
Six unrelated type-error pockets that were each blocking a different
page from compiling clean. Grouped because none individually warrants
its own commit and they all touch the same module's call sites.

api-keys/+page.svelte
  - Removed the `key: undefined as unknown as string` workaround for
    stripping the secret from the local list. Replaced with a clean
    object-rest destructure that produces a row matching the ApiKey
    shape (no `key` field). The cast was the source of two type
    errors AND was lying about the runtime shape.
  - Badge `variant="secondary"` and `variant="outline"` aren't valid
    BadgeVariant — narrowed to `default` and `info` respectively.
  - Button `variant="destructive"` and Badge `variant="destructive"`
    don't exist in the shared-ui union — both → `danger`.
  - Rate-limit input bound a `number` to a `<Input>` component whose
    `value` is typed `string`. Switched to a string state and
    parseInt on submit. Prevents the binding cast that the type
    checker (correctly) rejected.

reset-password/+page.svelte
  - Calling `authStore.resetPassword(token, password)` with two args
    on a method that takes one (sends the reset email). The method
    that actually performs the reset is `resetPasswordWithToken`.
    Two args, no API contract change needed.
  - `<Input minlength={12}>` — minlength isn't a prop on the shared
    Input component (it's not a passthrough wrapper). Removed; the
    runtime check still gates submit.

dashboard/widgets/{Credits,Transactions}Widget.svelte
  - `let state = $state<...>(...)` — variable named `state` shadows
    the `$state` rune call, which TypeScript flags as
    "Block-scoped variable '$state' used before its declaration"
    + "Untyped function calls may not accept type arguments".
    Renamed both to `loadState`.

dashboard/widgets/TasksTodayWidget.svelte
  - Referenced `task.dueTime`, which doesn't exist on LocalTask
    (only `dueDate`, ISO timestamp). Dropped the dead branch — the
    time was already encoded in `dueDate` and the widget never
    surfaced anything actionable from it anyway.

skilltree/components/StatsOverview.svelte
  - Was manually wiring `.subscribe()` callbacks because the old
    queries.ts returned raw Dexie Observables. After the
    Observable→useLiveQueryWithDefault migration, those return
    `{value, loading, error}` instead — `subscribe` doesn't exist
    on them. Replaced the manual state plumbing with direct
    `.value` reads inside `$derived`. Net: less code, fewer
    levels of indirection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:48:28 +02:00
Till JS
05d9d1962c fix(shared-auth): proxy passkey/2FA/session methods through ManaAuthStore
The settings page in mana/web (and any future consumer that wants to
manage passkeys, 2FA, or sessions from the UI) was calling 11
methods on `authStore` that the wrapper had never exposed:
listPasskeys, registerPasskey, deletePasskey, renamePasskey,
listSessions, revokeSession, getSecurityEvents, enableTwoFactor,
disableTwoFactor, generateBackupCodes — all of which DO exist on
the underlying AuthServiceInterface but were silently dropped by
createManaAuthStore. Result: 17 type errors on settings/+page.svelte
and a complete dead-end for anyone trying to wire up the UI.

Fix: add thin passthrough wrappers in createManaAuthStore that
delegate to authService. Each handles the SSR/no-service case the
same way the existing methods do (return empty array or
{success:false} with a stable error message). enableTwoFactor and
disableTwoFactor additionally refresh the local user snapshot
after success because the JWT issued post-enrollment carries the
new flag and downstream UI gates on it.

Type fixes that fell out of touching settings/+page.svelte:
  - UserData.twoFactorEnabled?: boolean — optional flag on the
    public user shape. The TwoFactorSetup component reads it via
    `authStore.user?.twoFactorEnabled` to gate the enable/disable
    button; without the type the call site coerced through `any`.
  - CreditBalance.{freeCreditsRemaining,dailyFreeCredits}?: number
    — daily-free accounting fields the backend already returns but
    the local type was missing. Optional because not every backend
    deployment turns them on.
  - settings/+page.svelte: `authStore.user?.sub` → `?.id`. The
    public UserData shape uses `id`; `sub` is the raw JWT claim
    name and never made it onto the consumer type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:48:00 +02:00
Till JS
7ba381fde8 fix(mana/web/news): Bearer token + Dexie v4 schema upgrade
Two distinct bugs surfaced by the first browser-side end-to-end test
of the News module against the locally-managed cloudflared tunnel.

═══ 1. Onboarding loop on reload ═══

The news tables were originally added to db.version(1).stores(),
which violates Dexie's "never edit a published version" contract.
Existing browsers stuck at db.version(3) (after the body + who
upgrades) never trigger an upgrade for v1 changes, so the news tables
silently never get created on those IndexedDB instances. Writes to
preferencesTable.add() / .update() failed at the storage layer, the
preferences row was never persisted, and on reload usePreferences()
returned the DEFAULT_PREFERENCES fallback (onboardingCompleted: false)
which re-rendered the onboarding wizard.

Fix: move the five news tables out of db.version(1) into a fresh
db.version(4).stores({…}) block. Dexie sees the bumped version number
and runs the additive upgrade transaction on existing v3 IndexedDBs,
creating the missing tables. Brand-new IndexedDBs go straight to v4
and pick up the union of all four version blocks. Both paths now
have the news tables present.

═══ 2. /api/v1/news/feed → 401 Missing authorization header ═══

The news api.ts client was passing `credentials: 'include'` thinking
the cookie alone would carry auth through to mana-api. It does not —
apps/api's authMiddleware() reads the Authorization header and
ignores cookies. Every browser-side fetch returned 401, the feed
cache stayed empty, and the wizard's "Fertig" → ranked feed flow
silently failed.

Fix: add a small `authHeader()` helper that pulls the JWT from
authStore.getAccessToken() and attaches it as
`Authorization: Bearer …`, mirroring the pattern in
modules/planta/api.ts. Both `fetchFeed()` and `extractFromUrl()` now
go through it. Drops the cookie credential entirely since it was a
no-op anyway.

Also tidies a Svelte 5 `$props()` warning in modules/news/ListView.svelte
(empty destructure instead of binding to a `_props` const).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:42:49 +02:00
Till JS
9bf73fffa3 fix(help): correct broken imports + tighten SupportedLanguage typing
Two unrelated bugs in the @mana/help package surface that together
accounted for ~40 type errors:

Broken component imports
  Ten components inside packages/help/src/components/ were importing
  from `'../types.js'` and `'./content'` — neither path resolves.
  The actual files are at `../ui-types` (where FAQSectionProps,
  FeaturesOverviewProps etc. live) and `../content` (where FAQItem,
  FeatureItem, FAQCategory live). Fix the imports to point at the
  real files. ESM resolution doesn't need `.js` suffixes when
  TypeScript is feeding tsc, and the existing index.ts already
  re-exports under the correct paths.

  Net: -19 type errors across:
    ChangelogEntry, ChangelogSection, ContactSection, FAQItem,
    FAQSection, FeatureCard, FeaturesOverview, GettingStartedGuide,
    HelpSearch, KeyboardShortcuts

content/help/index.ts SupportedLanguage cast
  `getManaHelpContent()` was passing `currentLocale` (typed `string`)
  into FAQ rows that expect a `SupportedLanguage` enum — 9 errors
  from each FAQ row. Add a small `asSupportedLanguage()` guard that
  validates the locale string against the union and falls back to
  'de' for unknown values. Single source of truth lives next to
  the function that needed it.

  Net: -9 type errors.

Combined with the spiral-db dist rebuild (local-only, gitignored)
and the previous Observable migration commit, the total error count
drops from 418 → 115.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:13:18 +02:00
Till JS
b32de06f2a fix(mana/web): coerce $page.params.* away from string|undefined
SvelteKit types `$page.params.X` as `string | undefined` because the
runtime cannot prove a route param exists at the type level — even
if the route file lives at e.g. `[id]/+page.svelte` and TS knows the
folder name. Thirteen route files were passing the raw param into
functions that take `string`, producing 25 type errors of the shape:

  Argument of type 'string | undefined' is not assignable to
  parameter of type 'string'.

Fix: hoist the param into a local with `?? ''` at the top of the
script, then use the local everywhere downstream. Empty string is
a safe fallback because the consuming code (`useDeck('')`,
`getCollectionById([], '')`, etc.) all return null/undefined for
unknown ids — exactly what they'd do if the param were truly
missing at runtime, which can't happen given the matching route
folder.

Files touched (one param hoist each):
  calendar/event/[id]              eventId
  cards/decks/[id]                 deckId
  citycorners/.../locations/[id]   citySlug + locId
  citycorners/.../locations/[id]/edit  citySlug + locId
  gifts/redeem/[code]              code
  inventory/collections/[id]       collectionId
  inventory/collections/[id]/edit  collectionId
  inventory/items/[id]             itemId
  photos/albums/[id]               albumId
  picture/board/[id]               boardId
  storage/files/[folderId]         folderId
  zitare/lists/[id]                listId (new local, replaces inline use)
  g/[code]                         code

Net: -24 type errors. The lone remaining "string | undefined" error
is a different bug in inventory FieldDefinition typing — unrelated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:12:54 +02:00
Till JS
ff6118fc3b fix(mana/web): migrate liveQuery hooks to useLiveQueryWithDefault
Seven module query files were calling raw `liveQuery(async () => ...)`
from dexie and returning the resulting Observable<T>. Consumer code in
the route .svelte files then read `.value` (or `.current`) on those
observables, which doesn't exist on the Dexie type — TypeScript flagged
38 errors and the call sites were silently relying on a runtime
property that only happens to work because the Svelte reactivity layer
re-evaluates the access.

Migration: switch each `useXxx()` hook to wrap with the existing
`useLiveQueryWithDefault` from `@mana/local-store/svelte`. The wrapper
returns `{ value, loading, error }` (with `value` synced to a `$state`
under the hood), so call sites can read `.value` reactively without
casts. Each hook now provides a typed default array so the wrapper
infers the right shape on first render.

Modules migrated:
  - chat        — useAllConversations, useArchivedConversations,
                  useAllTemplates, useConversationMessages
  - citycorners — useAllCities, useAllLocations, useAllFavorites
  - memoro      — useAllMemos, useArchivedMemos, useMemoriesByMemo,
                  useAllMemoTags, useAllSpaces
  - nutriphi    — useAllMeals, useAllGoals, useAllFavorites
  - presi       — useAllDecks, useDeckSlides, useDeck
  - questions   — useAllCollections, useAllQuestions,
                  useAnswersByQuestion
  - skilltree   — useAllSkills, useAllActivities, useAllAchievements

Call sites cleaned up:
  - chat/[id], memoro/[id]: removed inline `as { value: T[] }` casts
    that were the workaround for the broken type
  - nutriphi/{,add,goals,history}/+page.svelte: `.current ?? []` →
    `.value` (the wrapper guarantees the default array, so the
    nullish coalesce was always dead)
  - questions/{,[id],new,collections}/+page.svelte: same `.current` →
    `.value` migration

Net: -38 type errors, no behavior change. The wrappers continue to
subscribe to the same Dexie liveQuery under the hood; only the
ergonomic surface changed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:12:15 +02:00
Till JS
0426b6677b fix(mana/web/nutriphi): widen goal state types from literal to number
The DEFAULT_DAILY_VALUES constants are declared `as const` so each
field's type is a literal (e.g. `2000`, `50`). When the goals page
seeded its $state with these constants, TypeScript inferred the state
type as the literal — and any user-input number assignment then failed
type-check with "Type 'number' is not assignable to type '2000'".

The error was hidden until earlier today: the goals page also has the
same .current pre-existing pattern that the rest of the nutriphi
routes had, and tsc was short-circuiting on the .current error before
reaching the literal-type assignment. Now that queries.ts has been
moved to useLiveQueryWithDefault, .current is gone and the literal
typing surfaces.

Fix: explicitly type each $state as `<number>` so the literal widens
to a regular numeric state slot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:12:13 +02:00
Till JS
3ef095aaff fix(mana-llm/ollama): pass response_format to Ollama + strip markdown fences
The Ollama provider was completely ignoring `response_format` from the
incoming OpenAI-compatible request. Two consequences:

  1. Clients that asked for `{"type":"json_object"}` or
     `{"type":"json_schema",...}` got back JSON wrapped in
     ```json ... ``` markdown fences, because Ollama defaults to
     conversational output.
  2. Strict downstream parsers (Vercel AI SDK `generateObject`,
     manual `JSON.parse`) failed to decode the response and threw,
     even though the underlying JSON was valid inside the fences.

Fix: when response_format is set, translate it to Ollama's native
`format` field:

  - `{"type":"json_object"}` → `format: "json"`
  - `{"type":"json_schema","json_schema":{"schema":{...}}}`
    → `format: <the schema dict>` (Ollama 0.5+ supports full JSON
    schemas in the format field)

Defensive belt-and-suspenders: a small `_strip_json_fences` helper
runs after the Ollama response is decoded and removes any leftover
```json ... ``` wrapping. Some older vision models still wrap
output in fences even when `format` is set; this catches them.

Streaming path is unchanged because the nutriphi/planta refactor uses
non-streaming `generateObject`. Streaming structured output with
Ollama deserves its own pass when someone actually needs it.

Discovered during the AI SDK + Zod refactor smoke test — neither the
old nor the new vision routes ever returned validated JSON locally
because of this bug. Production uses Google Gemini directly via
fallback so the issue was masked there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:12:01 +02:00
Till JS
3ccfc3be99 fix(api): correct mana-llm path prefix and model name in vision routes
Found while smoke-testing the AI SDK refactor: both nutriphi and planta
were calling `${MANA_LLM_URL}/api/v1/chat/completions` and passing
`gemini-2.0-flash` as the model name. Both wrong:

  1. mana-llm exposes routes under /v1/, not /api/v1/. The original
     pre-refactor code had the same bug — it predates this commit and
     was apparently never noticed because the photo workflow was never
     wired into the unified app's UI until last week. /api/v1 returned
     404 against the live mana-llm container; now we hit /v1.

  2. mana-llm's router parses model strings as `provider/model`
     (services/mana-llm/src/providers/router.py:_parse_model). Without
     a prefix, `gemini-2.0-flash` was being routed as
     `ollama/gemini-2.0-flash` and only worked via the auto-fallback
     to Google when ollama failed. Be explicit: `google/gemini-2.0-flash`
     hits the Google provider directly and skips the failed-ollama
     round-trip.

VISION_MODEL env var still wins over the default, so prod overrides
remain possible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:11:43 +02:00
Till JS
6c0f88f5a2 chore(infra): pre-commit validator for cloudflared-config.yml
Adds scripts/validate-cloudflared-config.mjs — a node-only validator
that lint-staged runs whenever cloudflared-config.yml is staged. The
goal is to catch the same failure modes that
`cloudflared tunnel ingress validate` would catch on the server, but
without requiring cloudflared to be installed on every dev box.

Checks:
  - YAML parses
  - tunnel: is a uuid
  - credentials-file: ends with .json and contains the tunnel id
    (warning when it doesn't — likely an out-of-sync remnant from a
    previous rebuild, exactly the failure mode that bit us in the
    first locally-managed switch)
  - ingress: is a non-empty array
  - every rule except the last has both hostname AND service
  - the LAST rule is the catch-all `service: http_status:NNN`
  - no duplicate hostnames (the most common copy-paste mistake)
  - service URLs look like http(s):// / ssh:// / http_status:NNN
    / unix:/ / hello_world
  - hostnames are lowercase dot-separated DNS labels (no spaces, no
    weird characters)

Wired into lint-staged.config.js with a single glob entry; the
existing eslint + prettier flow is unchanged.

Tested against the live cloudflared-config.yml (passes, 51 hostnames)
and a synthetic broken file (catches all 6 categories of error +
the credentials-file/tunnel id drift warning).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:02:51 +02:00
Till JS
77b2d1eb32 chore(infra): smarter tunnel rebuild — apex via API + sane probes
Two improvements to scripts/mac-mini/rebuild-tunnel.sh based on what
the first prod run actually surfaced.

═══ 1. Apex domain auto-fix via Cloudflare API ═══

`cloudflared tunnel route dns` cannot route the apex of a zone
(error code 1003: "An A, AAAA, or CNAME record with that host already
exists"). The CLI has no command to delete those records. The first
rebuild left mana.how returning 530 because the script silently
failed to route it and we had to fix the apex manually in the
dashboard.

The new `apex_route_via_api()` helper:
  - Detects apex hostnames by dot count (one dot → two-label name)
  - Uses $CLOUDFLARE_API_TOKEN if available
  - Resolves the zone id by name
  - Deletes any existing A / AAAA / CNAME records on the apex
  - Creates a fresh proxied CNAME pointing at <tunnel>.cfargotunnel.com
  - Cloudflare's CNAME flattening at the apex makes this work
    transparently

If $CLOUDFLARE_API_TOKEN is not set, the script logs a warning at the
top of step 6 and falls back to the old behavior (route fails, user
fixes the apex manually). The token needs Zone:DNS:Edit on the
target zone.

═══ 2. Smarter HTTP verification ═══

The first run reported "5 hosts down (404/000)" but those were all
backend services without a root handler — credits/media/llm/mana-api
all return 404 at `/` and 200 at `/health`. The verify pass was
flagging healthy services as down and made the rebuild look more
broken than it was.

New `probe_host()` tries `/health` first, falls back to `/` only if
/health returned 4xx, and prefers a 2xx/3xx root response over a 4xx
/health. `probe_is_down()` only counts 5xx and 000 (libcurl error)
as failures — anything in 1xx-4xx means the request reached the
origin and the tunnel routing is correct, which is the actual thing
the verify pass cares about. `probe_label()` adds a one-word health
summary so the verify log reads "200 ok" / "401 auth required" /
"404 routed (no handler)" / "530 tunnel error" instead of just bare
status codes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:52:40 +02:00
Till JS
a50dc82887 chore(infra): tunnel id 1435166a (locally-managed rebuild result)
After running scripts/mac-mini/rebuild-tunnel.sh, the old remotely-
managed tunnel bb0ea86d-... was deleted and a new locally-managed
tunnel 1435166a-... took its place. The script's in-place sed of
the repo file didn't actually persist (the server-side ~/.cloudflared/
config.yml was patched, but the repo file ended up identical to HEAD
because the dev box had a stale checkout that got pulled over).

This commit catches the repo file up to the new tunnel id so a fresh
clone + setup-cloudflared-service.sh run wires the right credentials
file from the start. cloudflared has been running fine on the new
tunnel id since the rebuild — it auto-resolved the credentials from
~/.cloudflared/cert.pem when the in-config tunnel id pointed at a
deleted tunnel — but the file should match reality regardless.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:29:29 +02:00
Till JS
1b26623129 fix(api/Dockerfile): copy @mana/shared-types into the build context
After the planta + nutriphi modules in apps/api started importing
shared Zod schemas from @mana/shared-types, the runtime crashed in
a restart loop with:

  error: ENOENT reading "/app/apps/api/node_modules/@mana/shared-types"

Same root cause as the @mana/media-client gotcha already in this
Dockerfile: the build context only includes the workspace packages
that are explicitly COPYed, and shared-types was missed when it
became a transitive dependency.

Add the COPY line and rebuild. Also extend the comment block to
make the rule explicit ("when adding a new @mana/* import to any
apps/api module, add the package here too").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:25:23 +02:00
Till JS
5aeae87474 feat(api/web): wire-format envelope versioning + Anthropic prompt-cache hints
Adds AI_SCHEMA_VERSION + AiResponseEnvelope<T> in @mana/shared-types so
every AI structured-output endpoint speaks { schemaVersion, data }.
Backend wraps via envelope() in each module routes.ts; frontend api.ts
unwraps via unwrapEnvelope<T>() which throws AiSchemaVersionMismatchError
on drift — actionable network-panel error instead of cascading
'field is undefined' bugs further down the stack.

Also adds providerOptions.anthropic.cacheControl on the system message
in nutriphi + planta routes via SYSTEM_CACHE_HINT. NO-OP today (Gemini
backend, ~50-token prompts under the 1024-token cache minimum) but
lights up automatically when mana-llm routes to Claude or prompts grow
past the threshold. ~5 lines per route, no risk.

System messages migrated from system: shorthand to a full messages[]
entry — the only way to attach providerOptions per-message in the AI SDK.

13 new tests in nutriphi/ai-schemas.test.ts cover the version constant,
the mismatch error shape, and Zod accept/reject for both schemas. Total
nutriphi + planta suite: 62/62.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:21:19 +02:00
Till JS
9d1b25130d fix(api/who): server-side validation of [IDENTITY_REVEALED] sentinel
The user asked "bist du kopernikus?" while playing Galileo. The
LLM correctly responded "Kopernikus? ... aber nicht meiner!" — and
then appended [IDENTITY_REVEALED] anyway. Game flipped to "won
in 2 messages" with Galileo's name revealed, even though the
guess was wrong.

This is gemma3:4b being lazy about the sentinel rule: any time the
user says "bist du <name>?", the model is biased toward emitting
the sentinel because the prompt mentions "errät den Namen". Weaker
LLMs in general struggle to follow strict negative instructions
when the trigger word is right there in the input.

Fix in three layers:

1. Server-side validation (the real safety net). When the LLM
   emits [IDENTITY_REVEALED], independently verify that the user's
   CURRENT message contains the canonical character name (or one
   of its significant parts) using the same matchesName helper
   the explicit /guess endpoint uses. If the LLM emitted but the
   user didn't actually name this character, strip the sentinel,
   log a who.sentinel_false_positive, and treat the reply as a
   normal turn. The legit cases — user actually said the right
   name — still flow through cleanly.

2. matchesName improvements. The previous logic only matched a
   single-word guess against name parts; "bist du leonardo?" would
   fall through and miss a real win. Rewritten to:
     a) exact normalized match
     b) guess contains the full name as substring
     c) guess contains any significant name part as a WHOLE WORD
   Plus a Set for the guessWords lookup so it's O(1) per part.

3. Tighter system prompt. Added explicit "Sentinel-Regel" section
   with two FALSCH examples ("bist du Tesla?" while playing Edison,
   "bist du ein Erfinder?") and two KORREKT examples. Doesn't fix
   the false-positive rate at the model level but reduces it.

Layer 1 is the load-bearing one — even if the LLM emits the
sentinel for the wrong reason, the server gates the reveal on
ground truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:21:14 +02:00
Till JS
2804072565 docs(sync): add SYNC_DEBUG runbook with new debug API in Schritt C
The runbook for diagnosing why pending changes aren't flushing on
mana.how. Was sitting untracked in the repo root for the last week
of debugging; committing now that the debug surface it depends on
(window.__unifiedSync + getDebugInfo() + the surfaced silent
failures) actually exists.

Three steps in dependency order:

  Schritt A — read _pendingChanges directly from IndexedDB to find
  out which appIds and collections are stuck. Output drives the
  appId choice for Schritt B.

  Schritt B — manual POST against /sync/{appId} with the JWT from
  localStorage. Status code mapping table tells you whether the
  bug is server-side (4xx/5xx) or client-side (200 → sync engine
  isn't running).

  Schritt C — read window.__unifiedSync.getDebugInfo() (newly
  exposed in this commit batch) to see channel state. Compare
  knownAppIds against the Schritt A output: any appId with pending
  rows but no channel will accumulate forever, and the new
  console.warn from sync.ts will already be naming it explicitly.

Schritt B is the diagnostic key — everything else follows from
its status code.

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