Makes the webapp's AI policy and the server's tool allow-list physically
impossible to drift. Adds the missing entries the guard caught on first
run: `complete_tasks_by_title`, `visit_place`, `undo_drink` now have
parameter schemas server-side too.
- `packages/shared-ai/src/policy/proposable-tools.ts`
- `AI_PROPOSABLE_TOOL_NAMES` as `const` array + literal union type
- `AI_PROPOSABLE_TOOL_SET` for set-membership checks
- Webapp `DEFAULT_AI_POLICY` derives its `propose` entries from the
shared list via `Object.fromEntries(...)` — adding a tool there is now
a one-line change in `@mana/shared-ai`
- mana-ai `AI_AVAILABLE_TOOLS`: module-load assertion compares its
hardcoded names against `AI_PROPOSABLE_TOOL_SET` and throws with a
pointed error on drift (extras in one direction, missing in the
other). Service refuses to start on mismatch — better than silent
degradation.
- Bun test (`tools.test.ts`) runs the same contract plus sanity checks
(non-empty description, required params carry docs). Vitest policy
test adds the symmetric check on the webapp side.
All three runtimes now green: webapp 66/66, shared-ai 2/2,
mana-ai 9/9 Bun tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Completes the off-tab AI pipeline. mana-ai now writes produced plans
back to `sync_changes` as a server-sourced Mission iteration; the webapp
picks it up on next sync and translates each PlanStep into a local
Proposal via the existing createProposal flow. User sees the resulting
ghost cards in the matching module's AiProposalInbox with full mission
attribution.
Server (mana-ai v0.3):
- `db/connection.ts` — `withUser(sql, userId, fn)` RLS-scoped tx helper
mirroring the Go `withUser` pattern (SET LOCAL app.current_user_id)
- `db/iteration-writer.ts`
- `planToIteration(plan, id, now)` — shared-ai AiPlanOutput → inline
MissionIteration with `source: 'server'` + status='awaiting-review'
- `appendServerIteration(sql, input)` — INSERT sync_changes row with
op=update, data={iterations: [...]} + field_timestamps + actor
JSONB={kind:'system', source:'mission-runner'}
- `cron/tick.ts` — after parse success: build iteration, append to
mission.iterations, persist via appendServerIteration. Stats now
include `plansWrittenBack`.
Actor union:
- `packages/shared-ai/src/actor.ts` + webapp actor: `system.source` gains
`'mission-runner'` so the server's own writes are attributed correctly
and distinguishable from projection/rule writes
Webapp:
- `data/ai/missions/server-iteration-staging.ts`
- `startServerIterationStaging()` subscribes to aiMissions via Dexie
liveQuery; on each Mission update, walks iterations looking for
`source='server'` entries that haven't been staged yet
- For each such iteration: creates a Proposal per PlanStep under
`{kind:'ai', missionId, iterationId, rationale}` so policy + hooks
fire correctly
- Writes proposalIds back into plan[].proposalId + status='staged' so
other tabs and app restarts skip re-staging
- Idempotent: in-memory `processedIterations` Set + durable
proposalId marker
- Wired into (app)/+layout.svelte alongside startMissionTick
- 3 unit tests: translate server iteration → proposal, skip
already-staged, ignore browser iterations
Full pipeline now: user creates Mission in /companion/missions →
mana-ai tick picks it up → calls mana-llm → parses plan →
writes iteration → synced to webapp → staging effect creates
proposals → user approves in /todo (or any module) → task lands with
`{actor: ai, missionId, iterationId, rationale}` attribution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single source of truth for AI Workbench types shared between the webapp
(Vite/SvelteKit) and the server-side mana-ai Bun service. Prevents the
two runtimes from drifting on prompt shape or mission structure.
- `@mana/shared-ai` package:
- `actor.ts` — Actor union (user | ai | system) + helpers, mirrors the
webapp's runtime type so server-side consumers parse incoming actors
without re-declaring
- `missions/types.ts` — Mission, MissionCadence, MissionInputRef,
MissionIteration, PlanStep, MissionState. Adds optional
`iteration.source: 'browser' | 'server'` to distinguish foreground
vs server-produced iterations (groundwork for proposal write-back)
- `planner/prompt.ts` — `buildPlannerPrompt` pure function
- `planner/parser.ts` — `parsePlannerResponse` strict JSON validator
- Vitest smoke tests (2) cover prompt → parse round-trip + unknown-
tool rejection
- Webapp:
- `missions/types.ts` re-exports from shared-ai, keeps webapp-local
`MISSIONS_TABLE` constant + `planStepStatusFromProposal` bridge
- `missions/planner/{types,prompt,parser}.ts` become re-export stubs
so existing imports keep working unchanged
- Existing webapp tests (60) continue to pass — the wire code didn't
move, just its home
Next: mana-ai service imports buildPlannerPrompt/parsePlannerResponse
from shared-ai + wires mana-llm + writes iteration back as a
'source=server' row (tracked in services/mana-ai/CLAUDE.md).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The prior dance (93bb94a12 drop .ts, bb278fb3c switch to .js) kept
breaking one consumer or the other:
- bare specifiers (no extension) satisfied svelte-check but broke the
Node ESM loader invoked via @tailwindcss/node during SSR — SSR of
every (app) route 500'd with ERR_MODULE_NOT_FOUND on 'theme'.
- .js extensions satisfied svelte-check and Vite but still broke the
Tailwind loader, because the files on disk are .ts — Node ESM walks
the actual filesystem and can't rewrite .js → .ts the way tsc does
at type-check time.
Flip the web app's tsconfig to "allowImportingTsExtensions": true and
put the .ts extensions back. tsc now accepts the imports, and Node's
loader finds the real file on disk. No build step, no emit, and the
shared-types package stays a pure source-only TS workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Commit 93bb94a12 dropped the extensions on shared-types re-exports
to make the web app's svelte-check pass (its tsconfig has no
allowImportingTsExtensions). That satisfied tsc but broke SSR: the
dev server tripped with ERR_MODULE_NOT_FOUND on every (app) route
because Node's native ESM loader (used by downstream tooling like
@tailwindcss/node) cannot resolve bare relative specifiers without
an extension, and only Vite-owned paths got the bundler-style
resolution the fix relied on.
Switch to the TypeScript-ESM idiomatic `.js` extension. tsc with
moduleResolution: "bundler" still type-checks against the actual
.ts source, and at runtime both Vite and Node resolve `.js` the
same way — no tsconfig flag flip required.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- shared-types/index.ts: drop .ts import extensions (web-app tsconfig
has no allowImportingTsExtensions; bundler resolution handles it)
- backup/format.test.ts: narrow buildZip return to Uint8Array<ArrayBuffer>
so the Blob() constructor accepts it under strict lib.dom
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
eventstream was confusingly branded "Events" in the app registry,
colliding with the real events calendar module. Renamed to activity
(DE: Aktivität) since it's a live activity feed across all modules.
cycles -> period (DE: Periode) makes the menstrual-tracking module
self-describing. Tables cycles/cycleDayLogs/cycleSymptoms renamed to
periods/periodDayLogs/periodSymptoms; field cycleId -> periodId;
TimeBlockType 'cycle' -> 'period'; domain event CycleDayLogged ->
PeriodDayLogged. Generic "cycle" usages (billing, lifecycle, breath,
bicycle, import cycles) left untouched.
Constant disambiguation: prior DEFAULT_PERIOD_LENGTH (bleeding days)
renamed to DEFAULT_BLEEDING_DAYS; prior DEFAULT_CYCLE_LENGTH (28d full
cycle) is now DEFAULT_PERIOD_LENGTH.
Pre-launch, no data migration needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Following the shared-icons fix (d5cabed14), audit every workspace
package's src/index.ts for top-level side effects and flag the
ones that are safe to tree-shake:
- Pure TS re-export barrels (types, theme, utils, llm, storage):
"sideEffects": false — lets Vite prune entire submodules when a
consumer only imports a subset of named exports. Matters most for
shared-llm where the orchestrator/BYOK branch isn't needed on
every route.
- Packages that ship .svelte components (branding, ui, links):
"sideEffects": ["**/*.svelte", "**/*.css"] — same tree-shaking
benefit for TS modules, but keeps Svelte component CSS injection
intact.
The state-holding submodules (shared-ui drag-state/toast,
shared-llm store, shared-links mutations) are still evaluated
whenever their exports are referenced, so behaviour is unchanged —
the flag only lets the bundler skip modules that aren't in the
dependency graph at all.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phosphor-svelte ships a 28 MB lib of per-icon Svelte components and
does not declare "sideEffects" in its own package.json. When
@mana/shared-icons re-exports that package without its own
"sideEffects" hint, Vite/Rollup conservatively assume every
transitive module evaluation might matter and cannot aggressively
prune unused icons across chunk boundaries.
Our re-exports (index.ts: `export * from 'phosphor-svelte'` + a small
name→component registry) are pure ESM barrels with no top-level
runtime code, so flagging the package as side-effect-free is safe
and lets the bundler drop unused icons and skip evaluating the
icon-registry module from chunks that only want a named icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug: setting taskOverrides['companion.chat'] = 'byok' didn't work
when the user's allowedTiers was empty/['none']. The tier-too-low
check in run() compared task.minTier ('browser') against userMaxTier
('none') and threw TierTooLowError before the override was even read.
Same issue in canRun() and candidateTiers().
Fix: when a per-task override exists, treat it as opt-in to that tier
even if not in the global allowedTiers. The override is the user's
explicit per-task signal — overriding the global default is exactly
what an override is for.
- run(): effectiveMaxTier = max(override, userMaxTier)
- candidateTiers(task, override): adds override to baseTiers
- canRun(): now passes the override to candidateTiers
The Companion chat now correctly uses BYOK when selected from the
toolbar, even if the user hasn't enabled BYOK in their global LLM
settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1-3 of BYOK support. Introduces a 5th LLM tier 'byok' that
routes to user-provided API keys via direct browser fetches.
shared-llm additions:
- LlmTier extended with 'byok' (rank 3, between mana-server and cloud)
- ByokBackend: LlmBackend implementation that delegates key lookup
to an app-provided resolver callback, then dispatches to the right
provider adapter
- 4 provider adapters:
- OpenAI (gpt-5, gpt-4o, o1 family)
- Anthropic (Claude Opus/Sonnet/Haiku 4.6) with CORS header
- Gemini (2.5 Pro/Flash) — REST API with different message format
- Mistral — OpenAI-compatible, reuses shared openai-compat adapter
- Pricing table for 20+ models with USD per 1M tokens
- estimateCost() + formatCost() helpers
Keys stay device-local (IndexedDB in next phase). Browser-direct
fetches mean keys never touch Mana's server.
Updates two existing tier maps (memoro DetailView, SourceBadge) to
include the new tier.
Planning doc at docs/architecture/BYOK_PLAN.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The PWA was configured end-to-end in vite.config.ts and built the
manifest + service worker correctly, but neither was ever loaded by
the browser — no <link rel="manifest"> in the HTML and no script
registering the generated registerSW.js. Chrome therefore never fired
beforeinstallprompt, no install icon appeared in the URL bar, and the
hand-rolled PwaUpdatePrompt hung on navigator.serviceWorker.ready
because no SW had been registered.
Changes:
- Render pwaInfo.webManifest.linkTag into <svelte:head> in the root
layout so Chrome finds the hashed manifest.
- Replace the hand-rolled SW-update logic in PwaUpdatePrompt with
useRegisterSW() from virtual:pwa-register/svelte — it registers the
worker (immediate: true) and exposes reactive needRefresh +
updateServiceWorker stores that match registerType: 'prompt'.
- Add triple-slash refs to vite-plugin-pwa/info and /svelte in
app.d.ts for the virtual module types.
- Set manifest.id = startUrl in @mana/shared-pwa so Chrome doesn't
warn and keeps the install identity stable across start_url edits.
- Keep devEnabled: false and expand the comment: the 2026-04-08
dreams mic-button bug and the /offline navigateFallback both
misbehave when Workbox precache doesn't run under vite dev. Test
the install flow via `pnpm build && pnpm preview` instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Final optimization pass for the Companion Brain.
New modules (31 total):
- Meditate: MeditationCompleted event + log_meditation tool
- Sleep: SleepLogged event + log_sleep tool
Performance: DaySnapshot buildSnapshot() now runs all 6 Dexie
queries + 4 decryption passes in parallel via Promise.all instead
of sequentially. Estimated 3-5x speedup on first render.
Cleanup: trackActivity() in database.ts is now a no-op — the
_activity table is no longer written to. getRecentActivity() in
activity.ts delegates to queryEvents() from the Domain Event Store,
converting domain events to the legacy ActivityEntry shape.
Totals: 69 event types, 49 tools across 31 modules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Registers Mein Tag, Event Stream, Companion Chat, and Ziele as
workbench apps so they can be added to scenes alongside existing
modules like Todo, Calendar, etc.
New workbench pages:
- Mein Tag (myday): DaySnapshot overview — tasks, events, water
progress, nutrition, streaks at a glance
- Events (eventstream): live domain event feed with icons, labels,
and timestamps — shows the system "pulse" in real-time
- Companion (companion): embedded chat interface that auto-creates
a conversation on first use
- Ziele (goals): goal cards with progress bars, template picker
for quick goal creation, pause/resume/delete
Each page registered in both app-registry (workbench views) and
shared-branding (app metadata, icons, descriptions, tier=guest).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend: Hono/Bun service on port 3042 with JMAP client for Stalwart,
account provisioning (@mana.how addresses on user registration),
thread/message/send/label API endpoints, and JWT + service-key auth.
Frontend: Mail module with 3-column inbox UI (mailboxes, thread list,
detail/compose), local-first encrypted drafts in Dexie, and API-driven
thread fetching. Scoped CSS with theme tokens.
Integration: Dexie v11 schema, mail pgSchema in mana_platform,
mana-auth fire-and-forget hook for account provisioning,
getManaMailUrl() in API config, app registry + branding update.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New "Dehnen/Stretch" module for guided stretching with timer-based sessions,
mobility self-assessments, streak tracking, and configurable reminders.
Includes: 22 seed exercises, 5 preset routines (morning, desk break, evening,
upper body, lower body), fullscreen session player with Performance.now() timer
and Wake Lock, 6-step mobility assessment wizard with scoring, 30-day heatmap,
body region balance chart, custom routine builder, and reminder management.
Registered in module-registry, encryption registry (5 tables), database v9,
seed-registry, app-icons, mana-apps, and workbench app-registry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New "Rezepte" module following the established scoped-CSS + theme-token
pattern. Includes Dexie schema (v8), encryption for user-typed fields,
3 German seed recipes, search/filter/tag UI, inline creation form, and
expanded detail view with ingredients checklist and numbered steps.
Also documents the frontend styling inconsistency (13/40 ListViews use
Tailwind instead of scoped CSS) in docs/optimizable/ for future cleanup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline PillDropdownBar for user menu with a centered overlay
panel (UserMenuPanel). Move AI tier, theme, and language selectors
into the panel. Make app switcher and user pill icon-only. AI section
split into "Textgenerierung" and "Spracherkennung" subsections.
- AppDrawer trigger: icon-only (no label/chevron)
- User pill: icon-only, opens overlay panel instead of bar
- Theme + AI pills removed from nav bar (now in user panel)
- UserMenuPanel: centered on desktop, bottom-sheet on mobile
- Login button in footer, structured sections with subsection headers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New module for tracking all beverages (water, coffee, tea, juice, alcohol, etc.)
with daily progress bar, quick-tap presets, and inline editing of quantity/date/time.
Includes: module config, types, collections with guest seed (5 presets),
queries, store, ListView with context menus, route, app-registry registration,
Dexie schema v7, encryption registry, shared-branding icon/app entry.
Also extends docs/future/MODULE_IDEAS.md with additional module ideas.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PageShell header icon/title had opacity: 0.5 — removed for full
visibility. Moodlit, Zitare, Skilltree and BaseListView used
text-white/* classes that were invisible in light mode — migrated
to hsl(var(--color-foreground/muted-foreground)) tokens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PillNav overhaul:
- Dropdown-as-bar: theme/AI/sync/user menus render as horizontal
bars in the bottom stack (PillDropdownBar) instead of floating
popovers. New onOpenBar/activeBarId props on PillNavigation.
- iconOnly pills: tags/search/workbench-tabs pills show only icons.
Home pill removed. New iconOnly flag on PillNavItem.
- Segmented toggle groups: items sharing a `group` id render as a
single segmented pill (e.g. Light/Dark/System triple).
- Fullscreen mode: press "f" to hide all bottom chrome, Esc to exit.
- QuickInputBar + bottom bar visibility toggles via new pills.
- Progress ring on AI trigger pill during model download
(conic-gradient ::after, follows pill border-radius).
@mana/local-stt — new package for browser-local speech-to-text:
- Whisper models via transformers.js v4 (WebGPU + WASM fallback)
- Same Web Worker architecture as @mana/local-llm
- Two models: Whisper Tiny (150 MB) and Whisper Small (950 MB)
- Reactive Svelte 5 bindings (getLocalSttStatus, loadLocalStt, transcribe)
Voice-to-text integration:
- useLocalStt() composable: mic capture via AudioContext +
ScriptProcessor, resample to 16kHz mono, feed into Whisper worker
- Mic button in QuickInputBar (leftAction slot) with
recording/loading/transcribing states + pulse animation
- Transcribed text injected into InputBar via new injectedText prop
- STT model selector in AI bar alongside LLM tier controls
Also: vite.config.ts server.fs.allow expanded to monorepo root
so workspace package workers resolve in dev.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wallpaper system with four sources (predefined images, CSS gradients,
custom uploads via mana-media, and theme default). Configurable per-scene
or globally, with overlay controls (blur + opacity) and hover preview.
Adds sticky prop to shared PageHeader component and applies it across
themes, settings, credits, subscription, help, and profile pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch PageShell's per-theme paper overlay from a ::before +
mix-blend-mode + opacity stack to direct background-blend-mode on the
element itself. The old approach had invisibility issues in dark mode
and stacking-context quirks that made the grain disappear entirely.
background-blend-mode against background-color is the simpler, more
reliable primitive.
utils.ts auto-switches multiply → overlay in dark mode (dark × dark is
essentially invisible) while leaving other blend modes as-is. The
opacityLight/opacityDark knobs are gone from the paper config since
background-blend-mode has no opacity slot — tune via blendMode choice
instead.
Visual tuning pass:
- Card border bumped from 1px box-shadow ring to a real 2px border
with background-clip: border-box so the paper texture reads
continuously across the edge. Alpha 0.12 light / 0.28 dark (black).
- Drop shadow deepened (0 8px 24px + 0 3px 8px) for more card lift.
- Stone theme cooled toward real slate-blue: hue 200 → 212, saturation
bumped ~10pts across the palette. Stone was reading as warm-neutral
grey, now it's a proper cold blue.
- Texture remap: Lume → paper-004 (strongest grain, 480px tile for
coarser fiber), Stone → cardboard-002 (linen), Lavender → paper-001
(freed up after Stone claimed cardboard-002).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When users type 'mana.how' (no scheme), Safari and other browsers default
to HTTP. Cloudflare/cloudflared serves the page over HTTP without
rewriting the scheme. The browser then sends 'Origin: http://mana.how'
on every fetch, but mana-auth CORS only allows 'https://mana.how'.
Result: every auth request fails, the SSO check throws, AuthGate hangs
on the loading spinner forever, and the page never finishes loading.
Fix: detect HTTP requests in hooks.server.ts via cf-visitor /
x-forwarded-proto / event.url.protocol and 301-redirect to HTTPS before
serving any content. Localhost is exempted for dev.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Profile/Settings/Spiral/Credits move out of the standalone nav pills and
into the user-menu dropdown so the bottom bar stays compact. The dropdown
now also renders for guests (login users) — auth-only items (Profil,
Mana, Feedback, Logout) get filtered out, and a primary-styled "Anmelden"
entry replaces Logout. Themes is dropped from the dropdown since it
already has its own theme-variant pill.
New PillNavigation props: creditsHref, guestMenuLabel. New PillDropdown
icon paths: creditCard, spiral. New PillDropdownItem flag: primary
(prominent CTA styling), used for the guest Anmelden item.
All .glass-pill classes across PillNavigation, PillDropdown, PillTabGroup,
PillTagSelector, PillViewSwitcher, PillTimeRangeSelector, PillToolbar,
AppDrawer and ExpandableToolbar move from rgba+backdrop-blur to solid
theme tokens (hsl(var(--color-card)) / --color-border / --color-foreground)
so pills are fully opaque and follow the active theme variant instead of
having a frosted look that varied by background.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Settings page now uses a sidebar layout with category buttons (Profil,
Allgemein, KI, Sicherheit, Credits, Daten & Sync), an inline search field
that jumps to the matching section, and componentized sections under
lib/components/settings/. Each section owns its own data loading; the
+page.svelte shrinks from 617 to ~85 lines as a thin orchestrator.
The pill-nav AI tier dropdown now renders an icon per option (cpu, server,
cloud) and a power icon for the off state, and the "KI-Einstellungen"
shortcut deep-links to /settings#ai-options which auto-selects the KI tab
and scrolls to the panel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PillNavigation sync dropdown:
- New cloud icon pill showing sync status (Lokal/Sync/Pausiert)
- Dropdown with contextual actions: activate, top up credits, settings
- Shows next charge date when active
- Only visible for authenticated users
Onboarding wizard:
- New SyncStep between AI tier and Credits steps
- Explains local-first model: data always stays local, sync is optional
- Interval selection (monthly 30 / quarterly 90 / yearly 360 credits)
- Activate button with balance check and error handling
- Also fixed missing AiTierStep rendering in wizard template
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
All 37 MANA_APPS requiredTier set to 'guest' so every user
can access every module during the current testing phase.
Also resolves merge conflict in who/+page.svelte (formatting).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
Three independent fixes grouped because they're each one-line changes:
1. Revert MANA_APPS requiredTier test patch
Commit e52b6e29f flipped all 36+ apps to requiredTier='guest' for
local testing. Restored original tiers from before the flip:
guest-accessible (contacts, calendar, todo), public (who),
beta (zitare, calc, guides, arcade), alpha (most modules),
founder (memoro, nutriphi, mail, habits, notes, dreams, cycles,
events, finance, places, news). Body stays at 'guest' (new module,
intentional). The memory note "REVERT BEFORE RELEASE" is now done.
2. Widen toggleField to accept IndexableType keys
`toggleField<T>(table: Table<T, string>, ...)` rejected Dexie
tables keyed by IndexableType (the default). Changed the second
generic to IndexableType so callers like images.svelte.ts don't
need the `as unknown as Parameters<...>[0]` double-cast.
3. Add prepare script to spiral-db
`"prepare": "pnpm build"` ensures `dist/` is rebuilt after
`pnpm install` on a fresh clone. Without this, the 209 cascading
type errors from stale/missing dist files return on every new
checkout. Also added `prepublishOnly` as a safety net.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
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>
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>
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>