- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The media schema/tables were never created on fresh deploys because
mana-media only shipped a `db:push` script and nothing ever ran it
in the container. Result: every upload returned 500 the moment a
new environment came up (just hit prod again on mana.how).
- Add `db:generate` + `db:migrate` scripts and a migrate.ts runner
- Generate the initial migration covering media/media_references/
media_thumbnails (matches what was already on local + prod, which
were stamped manually so the migrator skips on existing deploys)
- Call runMigrations() at startup in src/index.ts so future fresh
containers self-bootstrap. Idempotent — drizzle tracks state in
drizzle.__drizzle_migrations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
Hit "container name already in use" / "removal in progress" errors
three times during today's Phase 5 deploys. The previous restart
pattern was just `compose up -d --no-deps`, which fails when:
1. A previous interrupted recreate left a stale container under
the canonical name. The new `up` tries to claim the name and
gets a conflict.
2. Compose's recovery from #1 sometimes creates a hash-prefixed
orphan container (`<hash>_<container_name>`), which then
blocks the next clean run too.
3. Even `--force-recreate` can't always handle the case because
the old container is in the middle of being removed when the
new one is being created (race).
Two-step replacement that's reliable across all three failure modes:
Step 1 — `docker compose rm -fs SERVICES`
Stops + force-removes the canonical compose-managed container.
Idempotent: does nothing if already gone. Filters out the
"No stopped containers" log noise so the output stays clean.
Step 2 — orphan sweep via `docker rm -f`
For each service, look up its container_name from the
compose config (falls back to the service name if not set),
then `docker ps -aq --filter name=^${cname}$` for the canonical
one and `name=_${cname}$` for hash-prefixed orphans. Anything
found gets nuked. This catches the case where compose's own
state has lost track of an orphan it created earlier.
Step 3 — `docker compose up -d --no-deps --remove-orphans`
Creates the fresh container. The `--remove-orphans` flag also
silences the "Found orphan containers ([mana-game-whopixels])"
warning we kept seeing — that's a leftover from a removed
service that nobody had cleaned up.
The container_name extraction uses awk on `compose config` output
(verified locally: `mana-web` → `mana-app-web`) so the script doesn't
need a hard-coded service→container mapping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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.
The first iteration of the Ollama response_format passthrough crashed
with 'ChatCompletionRequest object has no attribute response_format'
because the Pydantic request model didn't declare the field at all —
incoming response_format from OpenAI-compatible clients was being
silently dropped at the parsing layer before the provider could see it.
Fix: declare a typed ResponseFormat sub-model with the two OpenAI shapes
('json_object' and 'json_schema'), add it as an optional field on
ChatCompletionRequest, and let the Ollama provider read it directly
without defensive getattr fallbacks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
SvelteKit types `$page.params.X` as `string | undefined` because the
runtime cannot prove a route param exists at the type level — even
if the route file lives at e.g. `[id]/+page.svelte` and TS knows the
folder name. Thirteen route files were passing the raw param into
functions that take `string`, producing 25 type errors of the shape:
Argument of type 'string | undefined' is not assignable to
parameter of type 'string'.
Fix: hoist the param into a local with `?? ''` at the top of the
script, then use the local everywhere downstream. Empty string is
a safe fallback because the consuming code (`useDeck('')`,
`getCollectionById([], '')`, etc.) all return null/undefined for
unknown ids — exactly what they'd do if the param were truly
missing at runtime, which can't happen given the matching route
folder.
Files touched (one param hoist each):
calendar/event/[id] eventId
cards/decks/[id] deckId
citycorners/.../locations/[id] citySlug + locId
citycorners/.../locations/[id]/edit citySlug + locId
gifts/redeem/[code] code
inventory/collections/[id] collectionId
inventory/collections/[id]/edit collectionId
inventory/items/[id] itemId
photos/albums/[id] albumId
picture/board/[id] boardId
storage/files/[folderId] folderId
zitare/lists/[id] listId (new local, replaces inline use)
g/[code] code
Net: -24 type errors. The lone remaining "string | undefined" error
is a different bug in inventory FieldDefinition typing — unrelated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Seven module query files were calling raw `liveQuery(async () => ...)`
from dexie and returning the resulting Observable<T>. Consumer code in
the route .svelte files then read `.value` (or `.current`) on those
observables, which doesn't exist on the Dexie type — TypeScript flagged
38 errors and the call sites were silently relying on a runtime
property that only happens to work because the Svelte reactivity layer
re-evaluates the access.
Migration: switch each `useXxx()` hook to wrap with the existing
`useLiveQueryWithDefault` from `@mana/local-store/svelte`. The wrapper
returns `{ value, loading, error }` (with `value` synced to a `$state`
under the hood), so call sites can read `.value` reactively without
casts. Each hook now provides a typed default array so the wrapper
infers the right shape on first render.
Modules migrated:
- chat — useAllConversations, useArchivedConversations,
useAllTemplates, useConversationMessages
- citycorners — useAllCities, useAllLocations, useAllFavorites
- memoro — useAllMemos, useArchivedMemos, useMemoriesByMemo,
useAllMemoTags, useAllSpaces
- nutriphi — useAllMeals, useAllGoals, useAllFavorites
- presi — useAllDecks, useDeckSlides, useDeck
- questions — useAllCollections, useAllQuestions,
useAnswersByQuestion
- skilltree — useAllSkills, useAllActivities, useAllAchievements
Call sites cleaned up:
- chat/[id], memoro/[id]: removed inline `as { value: T[] }` casts
that were the workaround for the broken type
- nutriphi/{,add,goals,history}/+page.svelte: `.current ?? []` →
`.value` (the wrapper guarantees the default array, so the
nullish coalesce was always dead)
- questions/{,[id],new,collections}/+page.svelte: same `.current` →
`.value` migration
Net: -38 type errors, no behavior change. The wrappers continue to
subscribe to the same Dexie liveQuery under the hood; only the
ergonomic surface changed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DEFAULT_DAILY_VALUES constants are declared `as const` so each
field's type is a literal (e.g. `2000`, `50`). When the goals page
seeded its $state with these constants, TypeScript inferred the state
type as the literal — and any user-input number assignment then failed
type-check with "Type 'number' is not assignable to type '2000'".
The error was hidden until earlier today: the goals page also has the
same .current pre-existing pattern that the rest of the nutriphi
routes had, and tsc was short-circuiting on the .current error before
reaching the literal-type assignment. Now that queries.ts has been
moved to useLiveQueryWithDefault, .current is gone and the literal
typing surfaces.
Fix: explicitly type each $state as `<number>` so the literal widens
to a regular numeric state slot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Ollama provider was completely ignoring `response_format` from the
incoming OpenAI-compatible request. Two consequences:
1. Clients that asked for `{"type":"json_object"}` or
`{"type":"json_schema",...}` got back JSON wrapped in
```json ... ``` markdown fences, because Ollama defaults to
conversational output.
2. Strict downstream parsers (Vercel AI SDK `generateObject`,
manual `JSON.parse`) failed to decode the response and threw,
even though the underlying JSON was valid inside the fences.
Fix: when response_format is set, translate it to Ollama's native
`format` field:
- `{"type":"json_object"}` → `format: "json"`
- `{"type":"json_schema","json_schema":{"schema":{...}}}`
→ `format: <the schema dict>` (Ollama 0.5+ supports full JSON
schemas in the format field)
Defensive belt-and-suspenders: a small `_strip_json_fences` helper
runs after the Ollama response is decoded and removes any leftover
```json ... ``` wrapping. Some older vision models still wrap
output in fences even when `format` is set; this catches them.
Streaming path is unchanged because the nutriphi/planta refactor uses
non-streaming `generateObject`. Streaming structured output with
Ollama deserves its own pass when someone actually needs it.
Discovered during the AI SDK + Zod refactor smoke test — neither the
old nor the new vision routes ever returned validated JSON locally
because of this bug. Production uses Google Gemini directly via
fallback so the issue was masked there.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Found while smoke-testing the AI SDK refactor: both nutriphi and planta
were calling `${MANA_LLM_URL}/api/v1/chat/completions` and passing
`gemini-2.0-flash` as the model name. Both wrong:
1. mana-llm exposes routes under /v1/, not /api/v1/. The original
pre-refactor code had the same bug — it predates this commit and
was apparently never noticed because the photo workflow was never
wired into the unified app's UI until last week. /api/v1 returned
404 against the live mana-llm container; now we hit /v1.
2. mana-llm's router parses model strings as `provider/model`
(services/mana-llm/src/providers/router.py:_parse_model). Without
a prefix, `gemini-2.0-flash` was being routed as
`ollama/gemini-2.0-flash` and only worked via the auto-fallback
to Google when ollama failed. Be explicit: `google/gemini-2.0-flash`
hits the Google provider directly and skips the failed-ollama
round-trip.
VISION_MODEL env var still wins over the default, so prod overrides
remain possible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds scripts/validate-cloudflared-config.mjs — a node-only validator
that lint-staged runs whenever cloudflared-config.yml is staged. The
goal is to catch the same failure modes that
`cloudflared tunnel ingress validate` would catch on the server, but
without requiring cloudflared to be installed on every dev box.
Checks:
- YAML parses
- tunnel: is a uuid
- credentials-file: ends with .json and contains the tunnel id
(warning when it doesn't — likely an out-of-sync remnant from a
previous rebuild, exactly the failure mode that bit us in the
first locally-managed switch)
- ingress: is a non-empty array
- every rule except the last has both hostname AND service
- the LAST rule is the catch-all `service: http_status:NNN`
- no duplicate hostnames (the most common copy-paste mistake)
- service URLs look like http(s):// / ssh:// / http_status:NNN
/ unix:/ / hello_world
- hostnames are lowercase dot-separated DNS labels (no spaces, no
weird characters)
Wired into lint-staged.config.js with a single glob entry; the
existing eslint + prettier flow is unchanged.
Tested against the live cloudflared-config.yml (passes, 51 hostnames)
and a synthetic broken file (catches all 6 categories of error +
the credentials-file/tunnel id drift warning).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two improvements to scripts/mac-mini/rebuild-tunnel.sh based on what
the first prod run actually surfaced.
═══ 1. Apex domain auto-fix via Cloudflare API ═══
`cloudflared tunnel route dns` cannot route the apex of a zone
(error code 1003: "An A, AAAA, or CNAME record with that host already
exists"). The CLI has no command to delete those records. The first
rebuild left mana.how returning 530 because the script silently
failed to route it and we had to fix the apex manually in the
dashboard.
The new `apex_route_via_api()` helper:
- Detects apex hostnames by dot count (one dot → two-label name)
- Uses $CLOUDFLARE_API_TOKEN if available
- Resolves the zone id by name
- Deletes any existing A / AAAA / CNAME records on the apex
- Creates a fresh proxied CNAME pointing at <tunnel>.cfargotunnel.com
- Cloudflare's CNAME flattening at the apex makes this work
transparently
If $CLOUDFLARE_API_TOKEN is not set, the script logs a warning at the
top of step 6 and falls back to the old behavior (route fails, user
fixes the apex manually). The token needs Zone:DNS:Edit on the
target zone.
═══ 2. Smarter HTTP verification ═══
The first run reported "5 hosts down (404/000)" but those were all
backend services without a root handler — credits/media/llm/mana-api
all return 404 at `/` and 200 at `/health`. The verify pass was
flagging healthy services as down and made the rebuild look more
broken than it was.
New `probe_host()` tries `/health` first, falls back to `/` only if
/health returned 4xx, and prefers a 2xx/3xx root response over a 4xx
/health. `probe_is_down()` only counts 5xx and 000 (libcurl error)
as failures — anything in 1xx-4xx means the request reached the
origin and the tunnel routing is correct, which is the actual thing
the verify pass cares about. `probe_label()` adds a one-word health
summary so the verify log reads "200 ok" / "401 auth required" /
"404 routed (no handler)" / "530 tunnel error" instead of just bare
status codes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After running scripts/mac-mini/rebuild-tunnel.sh, the old remotely-
managed tunnel bb0ea86d-... was deleted and a new locally-managed
tunnel 1435166a-... took its place. The script's in-place sed of
the repo file didn't actually persist (the server-side ~/.cloudflared/
config.yml was patched, but the repo file ended up identical to HEAD
because the dev box had a stale checkout that got pulled over).
This commit catches the repo file up to the new tunnel id so a fresh
clone + setup-cloudflared-service.sh run wires the right credentials
file from the start. cloudflared has been running fine on the new
tunnel id since the rebuild — it auto-resolved the credentials from
~/.cloudflared/cert.pem when the in-config tunnel id pointed at a
deleted tunnel — but the file should match reality regardless.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After the planta + nutriphi modules in apps/api started importing
shared Zod schemas from @mana/shared-types, the runtime crashed in
a restart loop with:
error: ENOENT reading "/app/apps/api/node_modules/@mana/shared-types"
Same root cause as the @mana/media-client gotcha already in this
Dockerfile: the build context only includes the workspace packages
that are explicitly COPYed, and shared-types was missed when it
became a transitive dependency.
Add the COPY line and rebuild. Also extend the comment block to
make the rule explicit ("when adding a new @mana/* import to any
apps/api module, add the package here too").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds AI_SCHEMA_VERSION + AiResponseEnvelope<T> in @mana/shared-types so
every AI structured-output endpoint speaks { schemaVersion, data }.
Backend wraps via envelope() in each module routes.ts; frontend api.ts
unwraps via unwrapEnvelope<T>() which throws AiSchemaVersionMismatchError
on drift — actionable network-panel error instead of cascading
'field is undefined' bugs further down the stack.
Also adds providerOptions.anthropic.cacheControl on the system message
in nutriphi + planta routes via SYSTEM_CACHE_HINT. NO-OP today (Gemini
backend, ~50-token prompts under the 1024-token cache minimum) but
lights up automatically when mana-llm routes to Claude or prompts grow
past the threshold. ~5 lines per route, no risk.
System messages migrated from system: shorthand to a full messages[]
entry — the only way to attach providerOptions per-message in the AI SDK.
13 new tests in nutriphi/ai-schemas.test.ts cover the version constant,
the mismatch error shape, and Zod accept/reject for both schemas. Total
nutriphi + planta suite: 62/62.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>