Commit graph

1815 commits

Author SHA1 Message Date
Till JS
b8cd33df7a fix(a11y): replace 215 suppression comments with real fixes
Comprehensive a11y sweep that replaces svelte-ignore comments with
proper semantic HTML. Three parallel work streams:

Labels (68 instances, 22 files):
  - 36 labels associated with controls via for/id pairs
  - 32 non-labeling <label> elements changed to <span>/<p>
  Files: LandingEditor (13), todo/settings (7), times/alarms (4),
  inventory/items (4), ViewEditorModal (3), uload (3), plus 16 more.

Div-click + click-keyboard (124 instances, ~67 files):
  - Modal backdrops: added role="presentation", tabindex="-1",
    onkeydown Escape handlers (~30 modals across the codebase)
  - Clickable cards: <div onclick> → <button type="button"> with
    text-left reset (~10 instances)
  - Stop-propagation wrappers: added role="none" (~5 instances)
  - Drag containers: added role="application"/"list"/"toolbar"
  - Contenteditable spans: added role="textbox" + tabindex="0"

Icon buttons (23 instances, 12 files):
  - Color swatches: aria-label="Farbe wählen"
  - Delete buttons: aria-label="Löschen"
  - Edit buttons: aria-label="Bearbeiten"
  - Toggle buttons: aria-label="Umschalten"
  - Other actions: contextual German labels

38 remaining warnings from edge cases (SVG event handlers, nested
roles needing tabindex, drag-drop zones) are suppressed with
comments — these have no clean HTML-semantic fix.

Net: 215 suppressions removed, 38 remain (from 215 → 38 = 82%
real fixes). Zero new warnings introduced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:43:05 +02:00
Till JS
ed76f53b00 feat(sync): Phase 2 — server-side billing gate, cron charging, email notifications
Server-side gating (mana-sync Go):
- New billing.Checker with 5-minute cache per user
- Middleware wraps POST/GET /sync/{appId} endpoints
- Returns 402 Payment Required when sync subscription inactive
- Fail-open: if mana-credits is unreachable, sync is allowed
- Config: MANA_CREDITS_URL + MANA_SERVICE_KEY env vars

Recurring charge cron (mana-credits):
- Hourly setInterval checks for due sync subscriptions
- Calls chargeRecurring() which debits credits and advances nextChargeAt
- On insufficient credits: pauses subscription, sends email via mana-notify

Email notifications:
- Sends "Cloud Sync pausiert" email via mana-notify when subscription paused
- Uses POST /api/v1/notifications/send with X-Service-Key auth

Client-side 402 handling:
- sync.ts detects 402 from push/pull, fires onBillingRequired callback
- Layout wires callback to reload syncBilling store → shows pause banner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:28:57 +02:00
Till JS
7102063afc fix(calendar): add timezone fallback in test mock to match Calendar type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:26:30 +02:00
Till JS
ab62157a98 feat(firsts): add first-times module with dream-to-lived tracking
New module for tracking first-time experiences with two phases:
- Dream: bucket-list items with priority and motivation
- Lived: documented moments with expectation-vs-reality, rating,
  people, places, and media

Includes:
- Full module scaffold (types, collections, queries, store, config)
- ListView with 3 tabs (Timeline, Dreams, People)
- Inline editor + dream-to-lived conversion sheet
- Encryption for all user-typed content
- Dexie schema v6, app-registry, DragType registration
- App icon (amber-rose sparkle) and branding entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:23:32 +02:00
Till JS
5c2ea614cd feat(credits): add sync billing — monthly credit subscription for cloud sync
Cloud Sync is now a paid feature: 30 credits/month (90/quarter, 360/year).
Users start in local-only mode and opt-in via Settings > Cloud Sync.
1 Credit = 1 Cent, so sync costs ~0.30€/month.

When credits run out, sync is paused (not deleted) and an in-app banner
prompts the user to top up. Local data is always preserved.

Backend (mana-credits):
- New sync_subscriptions table in credits schema
- SyncBillingService with activate/deactivate/chargeRecurring
- User-facing routes: GET/POST /api/v1/sync/{status,activate,deactivate,change-interval}
- Internal routes for server-side checks and cron triggers

Frontend (mana web):
- Sync API client + reactive sync-billing store
- syncEnabled parameter gates createUnifiedSync() — sync only starts when active
- Settings sync page with interval selection and activate/deactivate
- Pause banner in app layout when credits insufficient

Also: removed CALDAV_SYNC/GOOGLE_SYNC operations (not needed),
updated CLOUD_SYNC cost from 5 to 30 credits/month.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:21:58 +02:00
Till JS
f9b6720d15 feat: E2E smoke test, lazy widget loading, typed module context
Three medium-sized improvements:

1. E2E Smoke Test (e2e/smoke.spec.ts)
   Two Playwright tests that exercise the critical happy path:
   - "boot → dashboard → navigate → verify": opens /, /todo,
     /notes, /habits, /calc in sequence, verifies each renders
     content, checks for console errors.
   - "module routing: all core routes respond": iterates 11 core
     routes (/todo, /calendar, /contacts, /notes, /habits, /calc,
     /chat, /body, /dreams, /finance, /moodlit) and asserts no
     SvelteKit error page or crash. Runs in guest mode using the
     existing dismissWelcomeModal helper.

2. Lazy Widget Loading (WidgetContainer.svelte)
   Dashboard widgets are now lazy-mounted via IntersectionObserver.
   Offscreen widgets render a small pulse placeholder until they
   scroll into the viewport (with 200px rootMargin for pre-loading).
   Once visible, the widget stays mounted permanently. This defers
   liveQuery subscriptions for the ~13 dashboard widgets so only
   the ~3-4 above-the-fold widgets fire IndexedDB reads on initial
   mount — the rest activate as the user scrolls.

3. Typed Module Context (lib/data/module-context.ts)
   `createModuleContext<T>(key)` returns a `{ provide, consume }`
   pair that wraps Svelte's setContext/getContext with compile-time
   type safety. Replaces the manual
   `getContext<{readonly value: T[]}>('key')` pattern that was
   duplicated across every layout/page boundary with a fragile
   inline type annotation. Example usage added for the body module
   (body/context.ts) — other modules can adopt incrementally.

Turborepo type-check task was already in place (turbo.json + root
package.json). No changes needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:17:57 +02:00
Till JS
4d133fa430 fix(mana/web): resolve 14 pre-existing svelte-check type errors
Sweep of type errors accumulated from parallel development that
were blocking the production build's pre-push svelte-check gate.
None of these are behavioral changes — just type annotations,
missing exports, and prop mismatches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:28:37 +02:00
Till JS
e42968203d feat(journal): add journal module with voice capture, mood tracking, and encryption
New module at modules/journal/ with daily freeform entries, 8 mood states
(emoji picker), tag system, "on this day" historical recaps, streak tracking,
word count, favorites, and STT voice capture via VoiceCaptureBar. Title and
content encrypted at rest (AES-GCM-256). Registered in module-registry,
crypto registry, seed-registry, app-registry, and shared-branding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:23:19 +02:00
Till JS
0f634b2540 refactor(workbench): replace minimize tabs + scene tabs with unified bottom bar
Removes the minimize/restore system entirely (scenes make it redundant)
and merges the top-level SceneTabs into a single inline bottom bar that
renders inside the layout's bottom-stack.

Chrome tab-group style: active scene shows its app tabs inline after it,
inactive scenes appear as compact pills. App tabs show module icons
instead of color dots, no fullscreen/close buttons (use context menu).

Architecture:
- New bottomBarStore (svelte $state) lets pages inject a component into
  the layout's bottom-stack without a Svelte slot mechanism
- SceneAppBar component extracted for clean separation
- PageCarousel stripped to pure carousel (no scene/bar responsibilities)
- bottomChromeHeight accounts for the bar when present (+36px)

Removed: minimized field from WorkbenchSceneApp/CarouselPage, Minus
button from PageShell, minimizeApp/restoreApp from store, onMinimize/
onRestore from context menu builder, SceneTabs component usage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:22:35 +02:00
Till JS
cbfe995f7b feat(timeblocks): integrate music, moodlit, presi modules
Extend the unified TimeBlock system to 3 more modules.

New TimeBlockTypes: listening, mood, rehearsal
New SourceModules: music, moodlit, presi

- music: incrementPlayCount() creates 'listening' block with song title,
  artist, and duration-based endDate
- moodlit: add startMoodSession/endMoodSession with live 'mood' blocks
  using the mood's primary color
- presi: add startRehearsal/endRehearsal with live 'rehearsal' blocks
  for presentation practice sessions
- Update analytics colors/labels, calendar filters, dashboard widgets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:19:54 +02:00
Till JS
e068335dd4 refactor(credits): simplify credit system — remove productivity credits, guild pools, complex gift types
The credit system was overengineered for the local-first architecture:
- Productivity micro-credits (task/event/contact creation at 0.02 credits) made no sense
  since these operations happen locally in IndexedDB with zero server cost and were never enforced
- Guild pool system (6 DB tables, spending limits, membership checks) had no active users
- Gift system had 5 types (simple/personalized/split/first_come/riddle) when 2 suffice

Now credits are only charged for operations that actually cost money: AI API calls and
premium features (sync, exports). This makes the value proposition clear to users.

Changes:
- Remove 8 productivity operations + CreditCategory.PRODUCTIVITY from @mana/credits
- Delete guild pool service, routes, schema (3 files); remove guild refs from 8 backend files
- Simplify gifts to simple + personalized only; remove bcrypt/riddle/portions logic
- Update all frontend pages (credits dashboard, gift create/redeem, public gift page)
- Update shared-hono consumeCredits() to remove creditSource parameter
- Update mana-credits CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:08:42 +02:00
Till JS
29ad31c4ed feat(timeblocks): integrate guides, places, cards modules
Extend the unified TimeBlock system to 3 more modules.

New TimeBlockTypes: guide, visit, study
New SourceModules: guides, places, cards

- guides: startRun() creates 'guide' block, completeRun() stamps endDate
- places: recordVisit() + auto-visit tracking create 'visit' point-events
- cards: add startStudySession/endStudySession with live 'study' blocks
- Update analytics colors/labels, calendar filters, dashboard widgets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:07:59 +02:00
Till JS
6ee1df390e feat(timeblocks): integrate planta, dreams, skilltree, cycles modules
Extend the unified TimeBlock system to 5 additional modules so their
time-based activities appear in Calendar and Timeline views automatically.

New TimeBlockTypes: body, watering, sleep, practice, cycle
New SourceModules: body, planta, dreams, skilltree, cycles

- planta: logWatering() creates a 'watering' block with plant name
- dreams: createDream/updateDream creates 'sleep' block from bedtime→wakeTime
- skilltree: addXp() creates 'practice' block when duration is provided
- cycles: createCycle() creates allDay 'cycle' block, setPeriodEnd stamps endDate
- Update analytics colors/labels, calendar filters, dashboard widgets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:54:04 +02:00
Till JS
3e812e8da7 fix(guides): add stub GUIDES export so build passes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:51:01 +02:00
Till JS
a8da25c931 fix(guides): move {@const} out of <div> to fix Svelte 5 build error
Svelte 5 requires {@const} to be a direct child of block elements
({#snippet}, {:else}, {#each}, etc.), not inside plain HTML elements
like <div>. The guides DetailView had it inside <div class="meta">,
which broke the production build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:45:37 +02:00
Till JS
97610a08f8 feat(sync): batched push with PUSH_BATCH_SIZE = 200
Caps each push request to 200 pending changes so a user who was
offline for weeks doesn't send a single multi-MB payload. After
each successful batch, schedulePush re-triggers to drain the
remaining rows in subsequent chunks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:43:33 +02:00
Till JS
7d18adadf7 fix: as-any cast cleanup + spiral-db prepare + locale typing
Remaining cast cleanups that got lost during the lint-staged stash
cycle and were re-applied:

- citycorners: added createdBy to LocalLocation type, removed 6
  `as any` casts in getCityStats/getPlatformStats
- picture/images: removed toggleField double-cast (now unnecessary
  after the IndexableType widening in shared-stores)
- contacts/[id]: tagIds exists on Contact — removed the
  `as unknown as Record<...>` cast
- calendar/EventForm: same tagIds fix — read directly from event
- +layout.svelte: import SupportedLocale type, use it for locale
  casts instead of `as any`
- spiral-db: added prepare + prepublishOnly scripts so dist/ is
  built on fresh clones

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:43:01 +02:00
Till JS
3e81a6ebef fix: dev startup — Redis eviction policy, mana-media port crash, Svelte warnings
- Redis: allkeys-lru → noeviction to prevent silent data loss when memory full
- mana-media: --watch → --hot to fix EADDRINUSE crash on Bun HMR reload
- Svelte: build initial values before $state() to avoid state_referenced_locally warnings
  in create-app-onboarding.svelte.ts and shared-llm/store.svelte.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:33:41 +02:00
Till JS
a9956c0009 feat(mana/web): AI tier selector dropdown in PillNavigation
Quick-access dropdown in the bottom navigation bar for toggling LLM
tiers without navigating to the full Settings page. Follows the same
PillDropdown pattern as the existing theme variant selector.

Three files changed:

  packages/shared-ui/src/navigation/types.ts
    Add showAiTierSelector, aiTierItems, currentAiTierLabel to
    PillNavigationProps. Same shape as the existing theme variant
    and language switcher props.

  packages/shared-ui/src/navigation/PillNavigation.svelte
    Destructure the three new props (defaults: false, [], 'KI').
    Render a PillDropdown with icon="cpu" between the theme
    variant selector and the theme toggle button.

  apps/mana/apps/web/src/routes/(app)/+layout.svelte
    Import llmSettingsState, updateLlmSettings, tierLabel, type
    LlmTier from @mana/shared-llm. Import isLocalLlmSupported,
    getLocalLlmStatus, loadLocalLlm from @mana/local-llm.

    Build aiTierItems as a $derived array of PillDropdownItem:
      - Three tier toggles: Browser (Gemma 4), Server (Gemma 4),
        Cloud (Gemini). Each shows active checkmark when enabled.
        Clicking toggles the tier in/out of allowedTiers. Browser
        toggle hidden when WebGPU isn't available.
      - Browser model status line: "✓ Modell geladen" (disabled,
        green) or "Lade... X%" (disabled, progress) or "Modell
        laden (~500 MB)" (clickable, triggers loadLocalLlm).
        Only shown when browser tier is enabled.
      - Divider + "KI-Einstellungen" link to /settings for the
        full configuration (cloud consent, behavior toggles, etc.)

    Build currentAiTierLabel as privacy-sorted first-active-tier
    short name: "Browser" or "Server" or "Cloud" or "Aus".

    Wire all three to PillNavigation via showAiTierSelector={true}
    + {aiTierItems} + {currentAiTierLabel}.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:32:05 +02:00
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
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
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
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