Patches header, action button, error banner, active/revoked sections
with pluralized counts, empty state, key list rows (rate badge,
created/last-used metadata, revoke button), how-to section,
create/success modal incl. all form labels and rate-limit hint.
Locale-aware Date via get(locale).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches form labels, event type options (now reactive via $derived),
source/comparison/period selectors, action buttons. Locale JSONs
landed in the previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches all toast/error messages, page header, action buttons, tabs,
received/created sections, create form, info card. Locale-aware
Date/number formatting via get(locale). Locale JSONs landed in the
previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches ListView, AssessmentWizard, ReminderManager, RoutineCreator,
SessionHistory, SessionPlayer, plus the /stretch route page title.
Locale JSONs landed in 421663ba3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds wardrobe namespace (de/en/es/fr/it) covering ListView,
GridView, OutfitsView, DetailGarmentView, DetailOutfitView,
GarmentForm, OutfitComposer, GarmentTryOnButton, TryOnButton,
TryOnModelPicker, CategoryTabs, GarmentCard, OutfitCard, plus
the /wardrobe/compose route. Categories/occasions/seasons routed
through dynamic `wardrobe.categories.{key}` lookups so constants.ts
keeps the order-tuples without leaking DE labels into UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ContextOverview ("Freundebuch" profile cards) was the single biggest
hardcoded-string hot-spot at 35 strings — every user sees this on their
profile. Extended `profile.context.*` namespace with section titles,
field labels (routine/social/leisure), placeholders, weekday short
names, and empty-state hints across DE/EN/ES/FR/IT.
Bonus: ratchet i18n-hardcoded baseline from 1879 → 1817 (settings
namespace + ContextOverview together cleared 62 violations).
- validate:i18n-parity: 39 namespaces × 5 locales — 3381 keys aligned
- svelte-check: 7647 files, 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final milestone of docs/plans/llm-fallback-aliases.md. Every backend
caller now requests models via the `mana/<class>` alias system instead
of hardcoded `ollama/...` strings. mana-llm resolves aliases through
`services/mana-llm/aliases.yaml` with health-aware fallback (M3) and
emits resolved-model + fallback metrics (M4).
SSOT moved to `packages/shared-ai/src/llm-aliases.ts` so apps/api,
apps/mana/apps/web, and services/mana-ai all import the same
`MANA_LLM` constant via the existing `@mana/shared-ai` workspace
dependency. Three additional sites (memoro-server, mana-events,
mana-research) inline the alias string with a SSOT comment because
they don't pull @mana/shared-ai today.
Migrated 14 sites across 10 files:
- apps/api: writing(LONG_FORM), comic(STRUCTURED), context(FAST_TEXT),
food(VISION), plants(VISION), research orchestrator (3 tiers
collapsed to STRUCTURED+FAST_TEXT/LONG_FORM)
- apps/mana/apps/web: voice/parse-task + parse-habit (STRUCTURED)
- services/mana-ai: planner llm-client + tick.ts (REASONING)
- services/mana-events: website-extractor (STRUCTURED, inlined)
- services/mana-research: mana-llm client (FAST_TEXT, inlined)
- apps/memoro/apps/server: ai.ts (FAST_TEXT, inlined)
Legacy env-vars removed: WRITING_MODEL, COMIC_STORYBOARD_MODEL,
VISION_MODEL, MANA_LLM_DEFAULT_MODEL. The chain in aliases.yaml is
now the single tuning surface; SIGHUP reloads it without redeploys.
New `scripts/validate-llm-strings.mjs` regex-scans 2538 files for
hardcoded `<provider>/<model>` strings and fails the build if any
land outside the SSOT or the explicitly-allowed paths (image-gen
modules, model-inspector code, this validator itself, the registry).
Wired into `validate:all` next to the i18n + theme validators.
Verified: `pnpm validate:llm-strings` clean, `pnpm --filter @mana/api
type-check` clean, `pnpm --filter @mana/ai-service type-check`
clean. Web type-check has 2 pre-existing errors in
SettingsSidebar.svelte (i18n MessageFormatter type drift, last
touched in 988c17a67 — unrelated to this work).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two DetailView polish items.
Print / PDF (fixes M10's "Drucken / PDF" action):
- New <article class="print-target"> at the top of the route renders
just the title + current version content. Hidden on screen, only
visible under @media print so window.print() produces a clean
manuscript instead of dumping the whole workbench chrome.
- :global(body > *) toggle suppresses the surrounding SvelteKit /
workbench frame; the .shell + the per-card chrome are explicitly
display:none in print. @page margin: 2cm gives a readable page
with no further user setup.
- Body uses ui-serif so the printed prose looks like manuscript.
Keyboard shortcuts (DetailView document-level listener):
- ⌘G / Ctrl+G → generate / re-generate (was: only the button)
- ⌘⇧S / Ctrl+Shift+S → save checkpoint
- ⌘Z / Ctrl+Z → undo last refinement (only fires when refineUndo
is set; otherwise falls through to the textarea's
native undo as the user expects)
Buttons + the undo row carry the shortcut in their title attribute so
mouse-users discover them via tooltip.
i18n baseline +1 for DetailView (the new "(⌘Z)" tooltip suffix counts
as one additional German fragment per the validator's heuristic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more reference kinds the resolver previously stubbed out are now
fully wired through the briefing form into the LLM prompt.
- reference-resolver: three new resolveX functions.
* Kontext is a singleton per space (the picker uses a sentinel
targetId; the resolver ignores it and reads via scopedForModule
+ first non-deleted row). Decrypts content and trims to budget.
* Goal reads from companionGoals (plaintext today) and surfaces
title + description + status + current/target so the model can
tie the draft into the user's actual progress.
* MeImage reads from the space-scoped meImages table; encrypts
label + tags. Hands the model a textual descriptor (kind / label /
tags) since the binary blob can't help prose generation.
- ReferencePicker: three new kind-tabs (🗂 Kontext, 🎯 Ziel, 🖼 Bild).
Kontext renders as a single-click "Kontext-Dokument verknüpfen" entry
if the space has one (with /kontext deep-link otherwise). Goals
active-first, then archived/done. Me-images render with thumbnail +
label + tags. Live-resolved chips via labelFor() for all three.
- i18n baseline bumped by one for ReferencePicker (the new
"Kontext-Dokument verknüpfen" string is intentional, in line with
the rest of the picker's existing German labels).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires Calendar through the M8.1+M8.2 backbone: flipping an event to
'unlisted' now publishes a server-side snapshot, the visible link in
the DetailView/EventDetailModal opens a real /share/[token] page, and
recipients can download an .ics file for their own calendar.
Changes:
- lib/data/unlisted/resolvers.ts (new):
buildUnlistedBlob(collection, recordId) dispatcher.
buildEventBlob: load LocalEvent + linked TimeBlock, decrypt
client-side, return { title, location, startTime, endTime,
isAllDay, timezone }. Description, reminders, tagIds, calendarId,
color stay out of the blob — sensitive context the user didn't
consent to share by flipping a single flag.
- modules/calendar/types: CalendarEvent gains `unlistedToken: string`
(empty string when no active token). timeBlockToCalendarEvent
forwards from LocalEvent. Draft-event scaffold initializes empty.
- modules/calendar/stores/events:
setVisibility now coordinates with mana-api. Flip-to-unlisted:
build blob -> publishUnlistedSnapshot -> store server-issued
token in patch.unlistedToken -> commit local update. If the
server call fails, no local change happens (no drift).
Flip-from-unlisted: revoke server snapshot first, then clear
local token + commit visibility change.
deleteEvent: revoke active unlisted snapshot before tombstoning,
so the share-link dies in lock-step with the local delete.
updateEvent + updateSingleInstance fire-and-forget
refreshUnlistedSnapshot(id) so the published blob tracks any
whitelist-field edits. Failures log; the next successful
refresh heals.
New regenerateUnlistedToken(id): revoke + republish in one call,
returns the fresh token. Powers the "Neu erzeugen" UI.
- routes/share/[token]/+layout.svelte: minimal anonymous chrome —
no app nav, no auth, no Dexie. Light/dark via prefers-color-scheme.
Footer carries "Geteilt via Mana" + signup CTA.
- routes/share/[token]/+page.server.ts: SSR loader. Fetches
/api/v1/unlisted/public/:token, dispatches 404/410 cleanly,
sets Cache-Control: private, max-age=60 + X-Robots-Tag: noindex.
- routes/share/[token]/+page.svelte: dispatcher; renders
SharedEventView for collection='events', stub message otherwise.
- modules/calendar/SharedEventView.svelte: standalone public render —
big date, location, "Zum eigenen Kalender hinzufügen" .ics link,
optional expiry note. OG/Twitter meta tags for WhatsApp/Slack
preview embedding. Uses $derived everywhere so prop updates
propagate through reactive recompute.
- routes/share/[token]/ical/+server.ts: RFC 5545 builder. No npm
library — small enough to inline. Escapes per spec, CRLF endings,
DTSTART/DTEND swap between VALUE=DATE and UTC depending on isAllDay.
Wrong-collection requests get 400.
- modules/calendar/views/DetailView (Workbench) + components/
EventDetailModal (/calendar route): SharedLinkControls dropped in
below the visibility row when event.visibility === 'unlisted'
AND event.unlistedToken AND shareUrl computed. The URL is built
client-side via buildShareUrl(window.location.origin, token) so it
stays in sync with whichever host the editor is open on.
Verified:
- pnpm check (web): 7541 files, 0 errors, 0 warnings
- pnpm test calendar + website: 26/26
- typecheck of new resolver, store hooks, SSR loader, iCal builder
Manual test path:
1. Open /calendar event in Detail view, flip Sichtbarkeit -> "Per Link"
2. Server publishes snapshot, Dexie record gets the server token
3. SharedLinkControls appear with copy + regenerate + revoke buttons
4. Open the URL in incognito → SSR fetches snapshot, renders
SharedEventView with date / location / .ics download
5. Edit the event title back in the main app → snapshot auto-refreshes
(refreshUnlistedSnapshot fires after updateEvent succeeds)
6. Flip back to "Bereich" → snapshot revoked server-side; subsequent
incognito reloads return 410 Gone
Next: M8.4 — same wiring for Library + Places. Uses the same
infra (resolvers dispatcher, share dispatcher) — just adds two new
buildXBlob functions, two SharedXView components, and the store
hooks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stoppt das Wachsen des 1877-String-Backlogs hardgecodeter deutscher
User-facing Strings in .svelte Files. Per-file Count vs. committed
Baseline; Datei darf NIE über ihrer Baseline liegen, neue Files müssen
mit 0 Verstößen starten.
- Erkennt: placeholder/title/aria-label/label/alt mit Umlauten,
Text-Content `>Großbuchstabe…<` (ohne Interpolation).
- Aktuelle Baseline: 1877 Verstöße in 428 Files; jeder Fix ratchet't
den erlaubten Wert nach unten.
- Lokales Update nach gewolltem Wachstum: `pnpm run validate:i18n-hardcoded -- --update`.
- In validate:all + CI verdrahtet.
- Drift-Test bestätigt: ein zusätzlicher umlaut-Placeholder lässt die
Datei "2 (was 1, +1)" failen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Validator im Stil von validate:theme-parity. Scannt
apps/mana/apps/web/src/lib/i18n/locales/<namespace>/<locale>.json
und failt hart, sobald ein Locale-File vom kanonischen DE-Key-Set
abweicht (fehlende oder überzählige Keys).
- DE ist canonical weil fallbackLocale='de' in i18n/index.ts. Missing
keys führten zu mixed-language UI, extra keys sind tote Altlasten.
- In validate:all verdrahtet — CI failt ab sofort bei neuem Drift.
- Smoke-Test: 35 namespaces × 5 locales, 2724 canonical keys clean.
- Failure-Test bestätigt: künstlicher extra-key in apps/it.json führt
zu exit 1 mit klarer Fehlermeldung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Google's Gemini image edit family (Nano Banana) as a user-
selectable model for Wardrobe Try-On next to the existing OpenAI
path. Three concrete choices now expose themselves in the Solo and
Outfit Try-On buttons:
- openai/gpt-image-2 (default, falls back to gpt-image-1
server-side when the org isn't
verified)
- google/gemini-3-pro-image-preview (Nano Banana Pro — premium
identity / character consistency)
- google/gemini-3.1-flash-image-preview (Nano Banana 2 — newest,
fast, cheapest)
All three accept multi-image refs (face + body + garment) through
the same /api/v1/picture/generate-with-reference endpoint; the only
differences are the provider-specific request/response shape and
the model-id routing.
Server (apps/api/src/modules/picture/routes.ts):
- Guard now accepts `openai/*` and `google/*` prefixes and rejects
everything else as "not supported for edits". Each provider's key
is validated separately so missing GEMINI_API_KEY doesn't break
OpenAI calls and vice versa.
- New `callGeminiEdits(modelName)` helper mirrors the shape of
callOpenAiEdits: encodes the normalized PNG refs as base64
inline_data parts, POSTs to
generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
with responseModalities=["TEXT","IMAGE"] and imageConfig
(aspectRatio + imageSize), pulls the generated image out of
candidates[].content.parts[].inlineData.
- Our internal size strings map cleanly: 1024x1024 → 1:1 / 1K,
1024x1536 → 2:3 / 1K, 1536x1024 → 3:2 / 1K. Gemini 1K is enough
for the thumbnail sizes Wardrobe renders; going higher bloats
payload without visible gain.
- creditsFor() gains a google/ branch proportional to upstream
pricing (pro ≈ 18, 3.1-flash ≈ 6, 2.5-flash ≈ 5).
- Response `model` reports `${provider}/${modelUsed}` so the picture
row's model metadata is accurate across providers.
Client (apps/mana/apps/web/src/lib/modules/wardrobe):
- api/try-on.ts: export `TryOnModel` union + `DEFAULT_TRY_ON_MODEL`.
RunGarmentTryOnParams / RunOutfitTryOnParams gain an optional
`model` field, threaded through `callGenerateWithReference`.
- components/TryOnModelPicker.svelte: new segmented control, three
options with label + one-line hint. Grid-auto-fits so it reflows
on the narrow workbench card.
- components/GarmentTryOnButton.svelte + TryOnButton.svelte: both
mount the picker above the Sparkle CTA. `estimatedCredits` on the
button label updates live when the user switches model so the
cost signal matches what the server will actually charge.
Env (scripts/generate-env.mjs): GEMINI_API_KEY and GOOGLE_API_KEY
now propagate from the root `.env.development` into `apps/api/.env`
so mana-api can pick them up at boot. The route reads GEMINI_API_KEY
with GOOGLE_API_KEY as fallback, matching how mana-llm ships today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
push_schema used to print "Failed (may not have db:push script)" for
every non-zero exit, lumping real failures (stuck rename prompts,
pre-existing public enums) in with missing scripts. Now it prints the
real exit code and tails the last 5 lines of drizzle-kit output so the
root cause is visible without re-running by hand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two loose ends from M3/M4:
1. Tool_use_id-based error attribution in the persona-runner
-----------------------------------------------------------
The previous collectActionsFromMessage() flipped the *most recent*
ActionRow to 'error' when a tool_result carried is_error:true. That was
fine as long as Claude invoked tools strictly in sequence, but when
the planner pipelines multiple tools in one turn, a later tool_result
carries an earlier tool_use_id — the last-action fallback mis-
attributes the error.
runMainTurn() now keeps a tool_use_id → action-index Map for the
duration of the tick. On tool_use we stash block.id, on tool_result we
look up the exact ActionRow via tool_use_id and flip that one. The
"flip last" path survives as a pure fallback if a future SDK ever
ships a block without an id.
2. New audit:encrypted-tools script
-----------------------------------
scripts/audit-encrypted-tools.ts — loads registerAllModules() and
apps/mana/…/crypto/registry.ts, diffs every ToolSpec.encryptedFields
against the authoritative web-app ENCRYPTION_REGISTRY.
Catches three classes of drift:
- missing-table : tool declares a table the web-app doesn't encrypt
- field-drift : both agree a table is encrypted but the field lists
differ (half-encryption in the wire is silent death)
- disabled : web-app has enabled:false while the tool still
encrypts — advisory warning, not a fail
Negative-tested by injecting a deliberate drift on todo.create +
todo.list (shortened ENCRYPTED_FIELDS to ['title']); the auditor
flagged both tools with full field diffs, restore returned to green.
Wired into `pnpm run validate:all` so the contract survives future
edits on either side. Fills the M4 audit gap noted in
project_mana_mcp_personas.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continuation of docs/plans/mana-mcp-and-personas.md. Personas are the
auto-test users the M3 runner will drive — they're real Mana users
(kind='persona', tier='founder'), registered through the same Better
Auth pipeline as humans, just stamped differently and metadata-tracked
so the persona-runner knows how to role-play them.
Schemas (auth namespace — personas are 1:1 with users, no reason for a
separate platform.* schema that the plan originally sketched)
- userKindEnum ('human' | 'persona' | 'system') + users.kind column,
wired into better-auth additionalFields so the JWT/user object carry
the flag. Default 'human' keeps every existing user untouched.
- auth.personas — 1:1 descriptor (archetype, systemPrompt, moduleMix
jsonb, tickCadence, lastActiveAt). CASCADE from users.id.
- auth.persona_actions — tick-grouped audit of every tool call the
runner makes (toolName, inputHash for dedup, result, latency).
- auth.persona_feedback — structured 1-5 ratings per module per tick,
plus free-text notes. This is where the runner writes the
self-reflection step at end of each tick.
Admin endpoints (/api/v1/admin/personas, admin-tier-gated)
- POST / create-or-update by email. Uses auth.api.signUpEmail
if the user's new, then stamps kind+tier+verified
and upserts the personas row. Idempotent — safe to
re-run after catalog edits.
- GET / list with 7-day action count per persona.
- GET /:id detail + recent 20 actions + per-module feedback
aggregate.
- DELETE /:id hard delete. Refuses non-persona users as
defense-in-depth: an admin typo here would cascade
through the full user-delete chain.
Catalog + seed pipeline (scripts/personas/)
- catalog.json 10 handwritten personas spanning 7 archetypes
(adhd-student, ceo-busy, creative-parent, solo-dev,
researcher, freelancer, overwhelmed-newbie).
Five pairs of personas that will later share
family/team spaces (cross-space setup is deferred
to M2.d per the plan).
- catalog.ts zod-validated loader. Refines email to require
@mana.test TLD — non-existent, no bounce risk.
- password.ts deterministic HMAC-SHA256(PERSONA_SEED_SECRET,
email). No stored per-persona credentials; the
runner re-derives on every login. Refuses the
dev-fallback secret in production.
- seed.ts POST /admin/personas per catalog entry. Flags:
--auth=, --jwt=, --dry-run.
- cleanup.ts Hard-delete every live persona. Warns when the
live set drifts from the catalog.
Root package.json:
pnpm seed:personas
pnpm seed:personas:cleanup
Extends the ESLint root-ignore list with `scripts/**` so Bun-typed
utility scripts don't fail the typed-parser check they weren't opted
into. Consistent with the rest of scripts/ being .mjs+.sh.
To go live (user action):
pnpm docker:up
cd services/mana-auth && bun run db:push
export MANA_ADMIN_JWT=...
pnpm seed:personas
M2.d deferred: cross-space (family/team/practice) memberships between
persona pairs. Better Auth's org-invite flow is multi-step and would
roughly double the M2 scope; the persona-runner (M3) can operate in
personal spaces first, shared-space tests land as their own milestone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pieces of the same cleanup:
1. build-app.sh now passes `--env-file .env.macmini` explicitly via a
shared COMPOSE_ARGS array. Without it, docker compose silently fell
back to `.env` in the project root — a separate file that happened
to hold MANA_AUTH_KEK and other secrets that `.env.macmini` lacked.
deploy.sh, restart.sh, and the CD workflow already used the flag;
this aligns build-app.sh with the rest. Server-side .env.macmini
was reconciled 2026-04-23 with the union of both files, so the
duplicate `.env` is no longer needed.
2. .env.macmini.example now documents 7 keys the prod stack actually
depends on but that had never been listed: GOOGLE_GEMINI_API_KEY /
GOOGLE_GENAI_API_KEY (SDK aliases for Deep-Research + mana-ai),
MANA_AI_PRIVATE_KEY_PEM / MANA_AI_PUBLIC_KEY_PEM (Mission-Grant
keypair), MANA_AI_DEEP_RESEARCH_ENABLED + PUBLIC_AI_MISSION_GRANTS
(feature flags), MANA_CORE_SERVICE_KEY (legacy alias), and the STT/
TTS internal shared secrets.
Matrix-bot tokens deliberately left undocumented — no Matrix homeserver
in the current running stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third provider path to /api/v1/picture/generate that calls OpenAI
gpt-image-2 when model starts with "openai/". Supports n=1..4 batch
generation with character continuity, base64 response decoded server-side
and uploaded to mana-media for dedup + thumbnails. Credit cost scales
by quality (low=3, medium=10, high=25) × n.
Env plumbing:
- scripts/generate-env.mjs: new apps/api/.env stanza propagates
OPENAI_API_KEY + REPLICATE_API_TOKEN from .env.secrets
- .env.macmini.example: documents OPENAI_API_KEY for prod
Frontend /picture/generate: model + quality + aspect-ratio + batch-count
selectors, real fetch with auth, persists each image via imagesStore.insert
(encrypted + synced). Wrapped in ModuleShell variant=fill with back-arrow
to /picture and a live credit badge in the header actions slot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/(app)/invoices/[id] route bundle drops from **534 KB → 18.6 KB** by
moving PDF rendering behind dynamic imports.
Changes:
- views/DetailView.svelte: `await import('../pdf/renderer')` inside
renderPdf() + downloadPdf(), cached in a module-local ref.
- components/SendModal.svelte: same for openAndDownload().
- pdf/scor.ts (new): generateSCORReference extracted so the
invoices store can derive a reference string without pulling
swissqrbill/svg + pdf-lib into the list-view bundle.
- pdf/qr-bill.ts: re-exports generateSCORReference from scor.ts
for backward compatibility.
- stores/invoices.svelte.ts: imports from ../pdf/scor (light) instead
of ../pdf/qr-bill (heavy).
- index.ts: drop re-export of the PDF renderer from the module
barrel so `import ... from '$lib/modules/invoices'` never drags
pdf-lib in.
The heavy chunk (pdf-lib + swissqrbill, ~576 KB) now only loads when
a user actually opens an invoice detail — list views, create flow, and
all other routes stay lean.
20/20 qr-bill tests pass; svelte-check clean.
Bonus: scripts/audit-icon-usage.mjs (+ pnpm run audit:icon-usage)
audits @mana/shared-icons imports. Reveals 204 distinct icons across
the codebase, 199 of them at default weight but paying for all 6
Phosphor weights. Biggest offender: app-registry/apps.ts with 69
static icon imports accounting for ~290 KB of the shared 466 KB icon
chunk. Migration path for that is documented in
docs/optimizable/bundle-analysis.md §2 — next session's work.
docs/optimizable/bundle-analysis.md also updated with the root (app)
layout (260 KB) investigation notes (start/stop lifecycle hooks to
defer via idleCallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#6 test coverage (pivot to reporting): 34/653 tests currently fail
(in-flight spaces-foundation migrations). Hard coverage thresholds
aren't enforceable until the suite is green, so this session ships a
file-presence audit instead of line-coverage gates.
- scripts/audit-test-coverage.mjs — counts .svelte + .ts source files
vs .test.ts + .spec.ts per module. Reports total ratio, lists
modules with 0 tests + ≥3 source files (prioritised by size).
- pnpm run audit:test-coverage wires it into audit:*.
- docs/optimizable/test-health.md — state + prevention path + top
untested modules ranked by impact.
Current baseline: 2.6% file-level coverage. 66/78 modules have zero
tests. Biggest untested: times (32 src), articles (29), events (27),
inventory + skilltree (20 each).
#8 audit:all: single entry point for the reporting audits. Runs
port-drift + i18n-coverage + test-coverage in --summary mode. Distinct
from validate:all (which is gates, not reports).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each services/*/CLAUDE.md declares `## Port: NNNN` — the authoritative
per-service port spec (docs/PORT_SCHEMA.md is explicitly partially
aspirational). This audit verifies:
1. Declared port appears as a literal in the service's own source
(catches: moved port in code but forgot to update CLAUDE.md).
2. No two services claim the same port (catches: accidental
collision when scaffolding new services).
Current state: ✓ 15 services, all declared ports found in code, zero
collisions (mana-auth/geocoding/stt/tts/image-gen/voice-bot/mail/
credits/user/subscriptions/analytics/events/news-ingester/ai/research).
Report-only; not a CI gate. Run with `pnpm run audit:port-drift`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlayView used Tailwind palette classes for game-status feedback:
bg-emerald-500/10 + text-emerald-300 (won) → bg-success/10 + text-success
bg-amber-500/10 + text-amber-300 (lost) → bg-warning/10 + text-warning
border-red-500/20 + bg-red-500/10 +
text-red-300 (error) → border-error/20 + bg-error/10 + text-error
placeholder-white/30 focus:border-purple-400/50 → placeholder:text-muted-foreground/60 focus:border-primary/50
Semantic status now tracks the theme (errors are red in dark, darker red
in light, etc.) instead of being fixed hex ramps.
The `bg-purple-500` / `bg-purple-500/30` / `hover:bg-purple-600` classes
on the user's chat bubble and submit buttons STAY — purple is the who
module's primary identity colour (historical-deck accent `#a855f7` is
semantically the same hue). Documented in brand-literals.md §who.
Also harden two validators against mid-rename states where git ls-files
returns paths that aren't on disk yet — both now skip unreadable files
instead of crashing the pre-commit hook (caught while migrating who).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translation infrastructure (@mana/shared-i18n + svelte-i18n + 35
per-module locale files with ~3500 lines across de/en/it/fr/es) is fully
wired, but 65/78 modules still hardcode German in .svelte templates
rather than calling {$_('module.key')}.
Adds:
- scripts/audit-i18n-coverage.mjs — scans lib/modules/**/*.svelte for
hardcoded German keywords (Abbrechen, Speichern, Löschen, etc.) in
files that don't import $_(). Reports per-module hit counts,
bucket (FULL/PARTIAL/NONE), and whether the locale file exists.
Supports --summary and --top N flags.
- pnpm run audit:i18n-coverage wires it into the audit:* family
(reporting only, not a CI gate — existing debt would fail
validate:all otherwise).
- docs/optimizable/i18n-migration-inventory.md — priority list,
per-module workflow, and prevention plan.
Top offenders: broadcast (26 hits), articles (24), events (23),
invoices (22), quiz (20), stretch (20), library (19), profile (17),
skilltree (15, PARTIAL), calendar (14, PARTIAL). Modules without a
locale file (broadcast/articles/events/invoices/…) need the locale
stubs scaffolded first.
Real string migration is per-site careful work (key naming, 5-language
parity, UI visual QA) and is left for per-module follow-up sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plan-doc commits 129971ffc + 9db044178 dropped the
audit-theme-tokens → validate-theme-variables rename, the
validate-theme-tokens → validate-theme-utilities rename, the new
validate-theme-parity script, brand-literals.md, and the corresponding
package.json + lint-staged.config.js + themes.css wiring. The files
still existed on disk (git mv changes survived) but were untracked.
Restore the validator suite so `pnpm run validate:all` works again:
- validate:theme-variables (CSS var names: --muted → --color-muted)
- validate:theme-utilities (Tailwind: no white/N, no neutral palette)
- validate:theme-parity (every --color-* in :root ⇔ .dark + each
[data-theme="..."])
All three wired into validate:all and lint-staged. `pnpm run validate:all`
is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep 98 `transition-all` occurrences across 62 files and replace with
targeted Tailwind transition utilities. Motivation:
1. `transition-all` animates every property, including CSS custom-
property-backed colours. On first paint the vars may not have
resolved yet, producing the P5 "white-on-white until first
interaction" rendering bug. The same bug hit food/moodlit ListViews
in the earlier theme migration.
2. Specific transitions also perform better — no layout-property
interpolation overhead.
Codemod scripts/migrate-transition-all.mjs classifies each class
attribute by its sibling classes and picks one of:
- `transition-opacity` — icon fade on group-hover
- `transition-[width]` — progress-bar width anim
- `transition-[transform,colors,box-shadow]` — scaled buttons/cards
- `transition-[border-color,box-shadow]` — card hover:border+shadow
- `transition-colors` — default (card/row hover)
91 / 98 auto-classified, 7 hand-migrated:
- EntryItem → transition-[box-shadow] (ring fade)
- NutritionProgressWidget → transition-[stroke-dashoffset,stroke]
- OnboardingModal → transition-[width,background-color]
- times/reports (3×) → transition-[width] / -[height] (bar anims)
- presi/present → transition-[width,background-color] (dots)
svelte-check clean with 0 errors; validate:all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace raw white-alpha Tailwind utilities (text-white/x, bg-white/x,
border-white/x) with canonical theme tokens (text-foreground, bg-muted,
border-border, etc.) in cards, context, food, moodlit, storage, music
ListViews. Replace hardcoded hex badge/dot/phase colors in ai-missions
with success/warning/error/primary tokens.
Fix two transition-all bugs (food:160, moodlit:223) that prevented CSS
custom property colors from resolving on first paint under theme switches.
Add scripts/validate-theme-tokens.mjs to prevent regression; run via
pnpm run validate:theme-tokens. Not yet in validate:all — 12 modules
still use raw white utilities (citycorners, guides, inventory, memoro,
picture, plants, playground, presi, questions, times, uload, who).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three final pre-dogfood items:
1. drizzle.config: schemaFilter now includes 'broadcast' alongside
'mail'. Without this, `bun run db:push` skipped the broadcast
tables — schema existed in code but not in Postgres. Tested via
db:push + psql \dt (3 tables created: campaigns, events, sends).
2. .env.development: new MANA-MAIL SERVICE section with Stalwart
knobs + broadcast config (tracking secret, rate limits, send
throttle). DEV secret is explicitly labelled non-production —
prod rotates via env.
3. generate-env.mjs: new block writes services/mana-mail/.env on
`pnpm setup:env`. Mirrors the invoices / research / events
pattern. All 16 broadcast/mail vars flow through from SSOT.
Verified end-to-end:
- pnpm setup:env → services/mana-mail/.env contains
BROADCAST_TRACKING_SECRET + rate limits
- bun run src/index.ts → /health returns 200 with the new config
- psql → broadcast.campaigns / events / sends are materialised
Broadcast module is now fully ready to send real mail — nothing
else required before the first dogfood campaign.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>