Capture the surprises from the first deploy so the next rollout
(or rollback) has the full picture without spelunking logs:
- mana-research had never been started on the Mac-Mini, even though
it was defined in compose. First-boot via `docker compose up -d`.
- research.* schema is not auto-migrated on service boot — drizzle
push must be triggered explicitly: `docker exec mana-research
bun run db:push`. 5 tables created.
- GOOGLE_GENAI_API_KEY was missing in /Users/mana/.../mana-monorepo/.env.
Copied the local key over, with `.env.bak.pre-gemini-deep-research`
as rollback anchor.
- Redis NOAUTH fix (commit 4867300d0) referenced here.
- Smoke-test outcome documented: the 500 was mana-credits 404 on a
test user without a wallet row — expected, and it proves the whole
auth/dispatch chain up to the credits hop works.
- Also noted: mana-llm has the same bare REDIS_URL in compose
(out-of-scope for this deploy), and /providers/health does not list
async providers (known design gap).
Status header updated to reflect deploy completion. Flag stays off
(MANA_AI_DEEP_RESEARCH_ENABLED=false) pending explicit enablement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Redis runs with --requirepass, but mana-research was pointing at
redis://redis:6379 without credentials. Cache misses are not fatal
(the executor falls back to the upstream provider on every request)
but the NOAUTH error spam drowns real errors in logs/glitchtip.
Match the pattern other services use:
redis://:${REDIS_PASSWORD:-redis123}@redis:6379
Caught during the deep-research deploy smoke-test on 2026-04-22.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Turns on at-rest encryption for the four tables staged in Phase 2a.
New writes now encrypt the user-typed fields; future code paths read
via decryptRecords as normal (the modules already call decrypt on
read, no changes needed).
Flipped:
- globalTags.name — tag names can leak categorization intent
- tagGroups.name — same
- workbenchScenes.name/description — scene labels often encode
Space-specific context
- aiMissions.title/conceptMarkdown/objective — mission configuration
is user-authored
Deliberately unchanged:
- color / icon / groupId / sortOrder / openApps / wallpaper /
scopeTagIds / cadence / state / agentId — all structural, indexed,
or FK data needed for query paths
- agents.name stays plaintext per the prior design note (Actor
displayName cache key)
Migration approach — pre-live lenient: decryptRecords skips values
that aren't encrypted (isEncrypted gate in record-helpers.ts:256), so
existing plaintext rows stay readable after the flip. New writes
encrypt; existing rows get encrypted organically as the user edits
them. No Dexie migration needed. A post-login "encrypt-at-rest
sweep" over pre-existing rows is a follow-up if hard at-rest coverage
is required before launch.
Crypto audit: 196 Dexie tables (95 encrypted, +4 vs 91 before),
101 allowlisted plaintext. Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the userTagPresets loop: users can now create, set-default,
and delete presets from Settings → Tag-Presets, making the dropdown
in SpaceCreateDialog actually useful (before this, it only showed
"empty" / "copy-current" because no presets existed).
New settings category "Tag-Presets":
- searchIndex.ts: adds the category entry + anchor; sidebar picks it
up automatically since it iterates `categories`.
- TagPresetsSection.svelte: list + create + delete + set-default.
- settings/ListView.svelte: conditional render wiring.
The create flow is deliberately one-click: name the preset, hit
"Aus <activeSpace.name> erstellen", and we snapshot every non-deleted
tag + tagGroup in the active Space into the new preset (with
groupName denormalized so the preset is space-independent). The first
preset automatically becomes the user's default — subsequent ones can
be promoted via the star button.
No full per-entry editor in this commit. If the user wants to tweak a
preset's contents, they create a sibling Space with the preset,
modify tags there, and promote THAT Space's tags to a new preset.
Scope-creep avoidance for a feature whose main value is snapshotting,
not authoring.
Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The frontend-consistency-improvements.md was stale — it still listed
the 13 Tailwind-based ListViews as "to migrate" even though all 21
flagged modules shipped today (a2a43b1d5, 86c205ffc, 7d6a340b1, 52af8c0ce,
3e09ff66d). Rewrite to reflect the SHIPPED state and list the next
layer of open consistency work (i18n, Phosphor icons, cross-surface
theme parity).
New docs/optimizable/README.md is the master index. Consolidates:
- 🔴 Release blocker: tier-patch revert (links memory entry)
- 🟠 Tracked trackers: per-topic links with status
- 🟡 Small open items without a dedicated file: module-structure
audit, plan-inventory hygiene, memory-hygiene post-release,
cross-surface theme parity
- How-to: list of `pnpm run audit:*` commands for live metrics
Doesn't introduce new work items beyond what's already been discussed
this session — just gives them a home so future sessions can pick up
any one without re-discovery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the Phase 2d.5a helpers (applyPresetToSpace,
copyTagsBetweenSpaces) into the new-Space UX so users get a familiar
tag taxonomy in every Space they create, without manual re-entry.
The dialog gains a "Tag-Set" dropdown:
- "Leer starten" — new Space starts without any tags
- "Aus <current> kopieren" — clones the user's active Space's
globalTags + tagGroups as a one-shot snapshot (fresh ids, no live
link back to the source)
- <named-preset> — applies a userTagPreset snapshot, creating tagGroups
for each distinct groupName so the user's familiar grouping carries
over
Default pick (when the dropdown first renders):
- If the user has a default preset → that preset
- Else if currently in Personal → "copy-current"
- Else "empty" (safer inside shared Spaces — don't leak Team/Family
taxonomy into a new one by default)
Seeding runs BEFORE the Space activation switches context, so
copyTagsBetweenSpaces still sees the source-Space's tags as
read-scope. Seeding failures are caught and logged but deliberately
non-fatal — the Space is already created, the user can seed later
from inside it.
`<select>` styling piggy-backs on the existing .field input/textarea
rules (extends the shared selector list instead of duplicating).
Type-check + Svelte a11y check clean.
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>
Adds the two seeding helpers the Space-creation flow needs:
- applyPresetToSpace(presetId, targetSpaceId): one-shot-copies a
preset's frozen snapshot as fresh globalTags rows in the target
Space. Creates tagGroups for each distinct groupName so the user's
familiar grouping carries over. Not a live link — renaming the
preset afterwards doesn't rename applied tags.
- copyTagsBetweenSpaces(sourceSpaceId, targetSpaceId): duplicates
every non-deleted tag + tagGroup from one Space into another with
fresh ids. Powers the "copy tags from my current Space" option in
SpaceCreateDialog so solo-Space users don't have to build a named
preset before they inherit their existing taxonomy.
Both helpers explicitly stamp spaceId on every written row so the
write lands in the TARGET Space even while the caller's active-space
context is still the SOURCE Space (SpaceCreateDialog: create Space
→ apply preset → activate → reload). The Dexie creating-hook
normally stamps spaceId from getActiveSpaceId(); pre-populating it
makes the hook's `if undefined/null` guard skip.
Both run inside a single Dexie transaction so a mid-batch failure
doesn't leave a half-seeded Space.
Duck-typed LocalTagShape / LocalTagGroupShape local to this file —
the authoritative types live in @mana/shared-stores but importing
them here would create an awkward data-layer → shared-stores
dependency direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two purposes:
1. Make the phase-by-phase progress discoverable — future readers can
see at a glance what's shipped, which commit hash lands each
phase, and what's still open.
2. Flag the 2d.4 attribution oddity: the active-space handler API +
per-Space workbench-scenes localStorage + scene spaceId filter +
runAgentsBootstrap-on-space-change wiring landed inside commit
3b85d7d3d ("chore(bundle): add bundle-size audit") by accident,
when a parallel terminal session's git add -A scooped up those
staged files during a lint-staged rollback race. The commit
message understates the contents; code is correct and tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opt-in path for missions that want Gemini Deep Research Max (up to 60 min
per task) instead of the shallow RSS pre-research. Because Max runs well
past a single 60-second tick, the state is carried across ticks:
tick N: submit → INSERT mission_research_jobs row → skip planner
tick N+k: poll → still running → skip planner (metric pending_skips)
tick N+m: poll → completed → inject as ResolvedInput, DELETE row, plan
- ManaResearchClient talks to mana-research's new internal
/v1/internal/research/async endpoints with X-Service-Key +
X-User-Id. Graceful-null on transport errors so a flaky
mana-research never crashes the tick loop.
- New table mana_ai.mission_research_jobs with PK (user_id, mission_id)
— presence is the "pending" flag; delete-on-terminal keeps queries
trivial.
- handleDeepResearch() encapsulates the state machine; planOneMission
now returns a discriminated union (planned | skipped | failed) so
"research pending" isn't miscounted as a parse failure.
- Opt-in at TWO gates to keep cost in check ($3–7/task, 1500 credits
per run):
1. MANA_AI_DEEP_RESEARCH_ENABLED=true server-side (default off)
2. DEEP_RESEARCH_TRIGGER regex matches the mission objective
(strict: "deep research", "tiefe recherche", "umfassende
recherche", "hintergrundrecherche", "deep dive")
Falls back to shallow RSS when either gate fails or the submit
errors upstream.
- Prom metrics: mana_ai_research_jobs_{submitted,completed,failed}_total
labelled by provider, plus _pending_skips_total.
- docker-compose wires MANA_RESEARCH_URL + the opt-in flag and adds
mana-research to depends_on.
- Full write-up with real API response shape (outputs plural, not
OpenAI-style), step-3 MCP-server plan (security-gated, not built),
ops + kill-switch: docs/reports/gemini-deep-research.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New providers gemini-deep-research + gemini-deep-research-max on the
Interactions API (preview-04-2026). Submit/poll split, tier parameter
selects between standard (~minutes, $1–3) and max (up to 60 min, $3–7).
- Parser matches the real response shape: flat `outputs` array of
thought|text|image items, url_citation annotations without title,
`usage.total_input_tokens` / `total_output_tokens`.
- Route generalisation: /v1/research/async accepts `provider` with
default 'openai-deep-research' (backward compatible) and dispatches
to the right submit/poll pair.
- New internal service-to-service endpoint /v1/internal/research/async
gated by X-Service-Key + X-User-Id for credit accounting. Enables
mana-ai to drive deep-research jobs on the mission owner's wallet
without requiring a user JWT.
- Pricing: 300 credits (standard) / 1500 credits (max). Conservative
markup over the ~$3/$7 ceiling so the first runs can't surprise us.
- Docs: AGENT_PROVIDER_IDS + pricing + env map + auto-router stay in
sync; CLAUDE.md Phase 3b now current; API_KEYS.md references the
new providers under GOOGLE_GENAI_API_KEY.
Verified with a real smoke test against the Gemini API: submit + poll
both succeed, completed response parsed cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Articles ist jetzt als Workbench-App in apps.ts registriert
(icon BookOpen, collection 'articles', paramKey 'articleId') und
landet damit im Scene-App-Picker. HomeView/ListView/HighlightsView/
StatsView teilen sich eine neue ArticlesTabShell, die sowohl als
SvelteKit-Route als auch als Workbench-Karte rendert.
Shell (ArticlesTabShell.svelte):
- Top-Bar mit QuickAddInput (URL einfügen + Enter = Save + goto
Reader; kein Preview-Schritt) und Settings-Gear.
- Tab-Leiste darunter: Leseliste | Highlights | Favoriten | Stats.
Leseliste ist Default (initialTab='list').
- Tab-Wechsel läuft intern via $state + Svelte-Context — kritisch
für die Workbench-Karte, wo goto() den User aus der Karte kicken
würde. getArticlesTabContext() aus tab-context.ts gibt tief
verschachtelten Sektionen eine switchTo(tab)-API.
- Padding 1rem 1.25rem auf der Shell selbst — PageShell.page-body
hat null padding, sonst klebt QuickAdd am Card-Rand. Im Route-
Kontext addiert's sich zum (app)-Layout-Padding ohne zu viel.
Tabs:
- Leseliste (list): bestehende ListView mit optionalem
initialFilter-Prop. Continue-Reading-Strip (HomeSectionWeiterlesen
horizontal carousel) erscheint über den Filter-Chips wenn
status='reading'-Artikel existieren und filter ∈ {all, reading}.
Filter-Chips sind einzeilig + horizontal scrollbar mit
scroll-snap-Einrast; inaktive Chips haben jetzt sichtbare
Background-Füllung + Border via color-mix(currentColor) — adaptiv
fürs Theme.
- Highlights (highlights): HighlightsView unverändert (nur der
eigene Header + Zurück-Button raus, liegt jetzt in der Shell).
- Favoriten (favorites): ListView mit initialFilter='favorites' —
Shell-Shortcut auf den Filter.
- Stats (stats): neue StatsView mit Stats-Strip (savedThisWeek,
finishedThisWeek, avg reading time), Highlight-Counter, Top-
Sources und Archiv-Link.
Routes (unter (tabs)-Gruppe):
- /articles → initialTab="list" (Default)
- /articles/list → initialTab="list" (alias)
- /articles/highlights → initialTab="highlights"
- /articles/favorites → initialTab="favorites"
- /articles/stats → initialTab="stats"
Detail/Add/Settings bleiben bewusst ausserhalb — die haben ihren
eigenen Reader/Form-Chrome und sollen die Tab-Leiste nicht zeigen.
Neue Files:
- ArticlesTabShell.svelte (Tab-Host)
- tab-context.ts (Cross-Tab-Switch-Context)
- components/ArticleCard.svelte (shared Card aus ListView extrahiert,
row + compact Varianten)
- components/QuickAddInput.svelte (URL-Input aus HomeView extrahiert)
- components/HomeSectionSources.svelte
- components/HomeSectionStats.svelte
- components/HomeSectionWeiterlesen.svelte
- views/StatsView.svelte
- routes/(app)/articles/(tabs)/{+page,list,highlights,favorites,stats}
Gelöscht:
- HomeView.svelte (Overview-Tab wurde rausgenommen auf User-Feedback)
- HomeSectionFrisch/Highlights/Favorites (durch eigene Tabs ersetzt)
docs/plans/articles-homepage.md dokumentiert den Architektur-Plan,
inklusive der Entscheidung für "eine Card pro Domain, interne Tabs"
statt zwei separater App-Registrierungen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this commit, the bootstrap created one "Mana" agent per user.
After per-Space migration, every Space needs its own default agent so
Actor attribution shows the right identity and missions land in the
right Space. Three users with Personal + Family + Brand Spaces would
have ended up with three "Mana" agents in the picker — ugly and
confusing.
Now each Space type gets a name that reads naturally:
- personal → "Mana" (keeps legacy name + DEFAULT_AGENT_ID
so historic Actor.displayName on
pre-migration records still renders)
- family → "Familien-Helfer"
- team → "Team-Assistent"
- brand → "Brand-Assistent"
- club → "Verein-Helfer"
- practice → "Praxis-Assistent"
Stable id scheme:
- Personal: DEFAULT_AGENT_ID (legacy coupling with LEGACY_AI_PRINCIPAL)
- Others: `default:<spaceId>` (deterministic, collision-free)
Bootstrap bypasses the regular createAgent path (which enforces
global name-uniqueness) because the same name is legitimately repeated
across multiple Spaces of the same type. Deduplication happens via
getAgent(id) + Dexie's add-or-skip for cross-tab races instead.
ensureDefaultAgent() reads the active Space via getActiveSpace(); when
no Space is loaded yet (pre-bootstrap first boot) it falls back to the
Personal default. The per-Space re-run on onActiveSpaceChanged (Phase
2d.4) picks up the correct agent once loadActiveSpace resolves.
Type-check clean.
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>
Since Phase 2d.2 of the space-scoped rollout, each Space can have its
own kontextDoc. Before this commit, the module was a user-level
singleton keyed by id='singleton' — which meant Shared/Brand/Family
Spaces saw the user's Personal-Space bio as their AI planner context.
Changes:
- types.ts: relax LocalKontextDoc.id to plain string (was the literal
'singleton'). KONTEXT_SINGLETON_ID stays as an exported const so
legacy Personal-Space rows (stamped before the refactor) are
documented; no longer used at write sites.
- stores/kontext.svelte.ts: ensureDoc() finds the active-Space row via
scopedTable(), creates a fresh UUID row if absent. setContent /
appendContent operate on the found-or-created row's id. Personal-
Space's legacy 'singleton' row keeps rendering because the
`_personal:<userId>` sentinel is inside getInScopeSpaceIds()'s
returned set.
- queries.ts: useKontextDoc() mirrors the same scopedTable filter.
- ai/missions/default-resolvers.ts: kontextIndexer surfaces the active
Space's kontextDoc (not hardcoded 'singleton'). Shared-Spaces without
a doc yet return an empty candidate list, which is the correct
empty-state for the mission-input picker.
Type-check clean. No schema change; relies on v28's existing spaceId
stamping + the creating-hook's ongoing stamp (kontextDoc is in the
kontext module.config).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svelte-check emits a11y + dead-CSS + Svelte-5 $state warnings that were
previously non-blocking — pre-push only caught hard type errors. The
a11y-30 cleanup commit (3e09ff66d) brought the warning count to 0, so
flipping `--fail-on-warnings` on now makes the checker hold the line:
any new warning fails the pre-push hook that runs `pnpm check`.
Covers: a11y_click_events_have_key_events, a11y_consider_explicit_label,
css_unused_selector, state_referenced_locally, and the other svelte-check
diagnostic categories.
No behaviour change with current codebase (0 warnings); prevents drift
going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First wire-up of the userTagPresets surface from Phase 2b's v34 schema.
This is the store layer only — Space-create UI integration + the
apply-preset-to-space flow land in a follow-up commit alongside the
SpaceCreateDialog changes.
- lib/data/tag-presets/types.ts: LocalUserTagPreset shape + inline
TagPresetEntry + toUserTagPreset converter.
- lib/data/tag-presets/store.svelte.ts: createPreset / updatePreset /
deletePreset / setDefault / appendEntry. Stamps userId explicitly
because userTagPresets is kept out of SYNC_APP_MAP (the Dexie
creating-hook only fires for sync tables). At-most-one-default-per-
user invariant enforced by clearDefaultFlag() before writes that set
isDefault=true.
- lib/data/tag-presets/queries.ts: useUserTagPresets + useDefaultTagPreset
live queries. User-scoped, no active-space filter (presets show from
any Space context).
- crypto: move userTagPresets from plaintext-allowlist to
ENCRYPTION_REGISTRY with fields ['name', 'tags']. AES wrapping handles
the tags array via JSON-stringify, same pattern as food.foods.
Crypto audit: 196 tables (95 encrypted, +1 userTagPresets). Type-check
clean.
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>
Adds the two small schema changes needed for the space-scoped rollout.
The heavy lifting (userId drop, kontextDoc reshape, store-API pivots)
lives in 2c / 2d — this commit keeps scope surgical to isolate the
Dexie version bump.
1. userTagPresets table — user-level templates for seeding tags into
newly-created Spaces. Deliberately NOT space-scoped: the preset
picker runs from ANY Space during new-Space creation, so active-
space filtering would hide the user's other presets. Indexed on
userId + isDefault. NOT yet in SYNC_APP_MAP — cross-device sync
wires up in 2d alongside the CRUD store API.
2. Compound indexes on globalTags + tagGroups:
- globalTags: [spaceId+sortOrder] (per-Space sorted list) +
[spaceId+name] (in-Space dedup-by-name)
- tagGroups: [spaceId+sortOrder]
Tags/tagGroups already carry spaceId on every row (v28 migration +
the creating-hook's ongoing stamping), these indexes simply let
per-Space queries skip the client-side JS filter that
scopedForModule does today.
Audit script pass-through: userTagPresets sits on the plaintext
allowlist with a comment flagging that it moves to ENCRYPTION_REGISTRY
in 2d once the store API wraps writes. 196 Dexie tables classified,
type-check clean.
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>
Preparation step for the space-scoped data model migration (Phase 2b).
Moves globalTags, tagGroups, workbenchScenes, and aiMissions from the
plaintext allowlist into the encryption registry with enabled:false —
so the audit script documents which fields WILL be encrypted without
changing any runtime behaviour.
Fields chosen per design-doc:
- globalTags.name — personal categorization (Therapie, Finanzen-privat)
- tagGroups.name — same
- workbenchScenes.name + description — scene labels often encode
Space-specific context (Q2-Launch, Urlaub 2026)
- aiMissions.title + conceptMarkdown + objective — all user-typed
mission config; state/cadence/inputs stay plaintext for the Runner
Deliberately kept plaintext (against my initial suggestion):
- aiAgents.name — registry comment explains: name is the Actor
displayName cache key for historic attribution. Encrypting would
show "🤖 [encrypted]" on every past task the agent ever touched.
- globalTags.icon / tagGroups.icon / color — not personal content;
icon is a visual cue, color is theme metadata
The 2c migration (Dexie v35, flip enabled:true) runs after 2b lands
the schema changes so existing rows get encrypted in one controlled
pass instead of mixing schema + encryption in the same upgrade.
Crypto audit: 195 Dexie tables classified (94 encrypted, 101
plaintext-allowlisted). Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svelte-check now completes clean (0 errors, 0 warnings, 0 files with
problems).
- profile/ContextOverview: 11 click-on-div sites made keyboard-
accessible with role="button", tabindex="0", and an onActivate helper
that fires the same handler on Enter/Space. Two <p> wrappers became
<div> since <p> cannot carry role="button" per ARIA.
- profile/ContextInterview: paginate dots got aria-label + aria-current.
- settings/GeneralSection: toggle button got aria-label +
aria-pressed.
- events/RegionPicker: radius label associated with range input via
for/id.
- events/SourceManager: drop unused .source-item.inactive + .inactive-
badge CSS selectors (dead code).
- research-lab/CompareColumn: local `rating` seed from entry.userRating
now uses svelte-ignore comment + $effect sync (intentional seed-only
read, plus prop-update mirror).
- admin/ListView: initialTab prop is deliberately read only at mount;
svelte-ignore comment documents the intent.
- gifts/redeem: drop unused .animate-fade-in CSS selector.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit of every Dexie table in apps/mana/apps/web/src/lib/data/database.ts
+ crypto registry finds no blockers for Phase 2, with two scope
adjustments to fold in:
1. Add agentKontextDocs (v22) to the to-migrate list. Per-agent
context docs reference the aiAgents table; migration order must
be agents first, then backfill agentKontextDocs.spaceId via
parent-agent lookup.
2. The 46 already-space-scoped tables from the Spaces-Foundation
sprint all still carry userId alongside spaceId. To hit the
"no table has both userId and spaceId" invariant, Phase 2
extends from just the 7 newly-migrated tables to a ~53-table
sweep dropping userId everywhere. Mechanically identical per
table, so the extra scope is cheap.
Also confirmed:
- All 19 junction tables have space-scoped parents — no dangling
refs. Safe to migrate parents.
- Actor columns (__lastActor / __fieldActors) stamped everywhere by
the Dexie creating hook — userId can be dropped confidently.
- userContext (v23 profile hub) is distinct from kontextDoc (AI
planner injection). userContext stays user-level; kontextDoc
moves per-Space. No collision.
- 10 user-level singleton tables correctly identified to stay
user-level (userSettings, newsPreferences, meditateSettings, …).
- 10 internal/infra tables (_pendingChanges, _events, _aiDebugLog,
…) get per-table treatment; mostly no spaceId needed.
Phase 2 can proceed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-audit of the previous draft surfaced 7 legacy residues that would
have left the rebuild short of the "optimal architecture" bar. Rewrite
the plan with those addressed:
1. Drop userId from data records entirely. Attribution lives in the
Actor system (__lastActor / __fieldActors). userId stays only on
explicitly user-scoped tables.
2. Active scene localStorage key becomes per-Space:
`mana:workbench:activeSceneId:${spaceId}` — switch Space A → scene
X, to B → scene Y, back to A → X restored.
3. New user-level userTagPresets table replaces the "copy from
Personal" checkbox hack. First-class templates for seeding new
Spaces with a named tag set; CRUD in Settings.
4. Encryption decision made in-line: globalTags + tagGroups names
encrypted during migration, not deferred (tag names like
"Therapie" or "Finanzen-privat" can leak personal categorization).
5. kontextDoc moves from user-level singleton to per-Space. AI runner
pulls the active Space's kontextDoc; Shared-Spaces start without
one until the user writes one.
6. Default-agent bootstrap uses SpaceType-aware names (Mana for
personal, Familien-Helfer for family, Team-Assistent for team,
etc.) so users don't end up with "three Mana" in their agent list.
7. Phase 1 explicitly audits every junction table to verify parent
records carry spaceId — no silent user-global references.
Also: an explicit "No legacy residues" section anchors these as
intentional anti-patterns to prevent drift. Success criteria now
includes "no table has both userId AND spaceId" as a testable
invariant.
Timeline grows from 3–4 to 4–5 days; the delta is encryption wiring
+ userTagPresets CRUD + the userId→Actor cleanup.
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>
Reader page is now a proper distraction-free reading surface instead
of a padded card inside the (app) layout.
Layout:
- .detail-shell breaks out of the (app) layout's padded + max-width
container via the 100vw + negative-margin-X trick, and additionally
cancels the vertical padding (<main pt-2> + inner py-2) plus the
bottom-chrome reservation. The reader theme therefore paints
edge-to-edge including behind the PillNav. No more island-in-a-sea
look.
- Initial theme (light/sepia/dark) mirrors the global Mana theme at
mount time by checking document.documentElement.classList.dark — so
opening an article from a dark-mode app no longer flashes a white
reader. User can still override per-article via the swatches.
Toolbar unification:
- Old two-bar layout (top: back + typography, bottom: actions) fused
into one floating pill-bar at the bottom. Three groups divided by
vertical rules: nav | typography | actions. flex-wrap handles narrow
screens gracefully.
- position: fixed + bottom: calc(--bottom-chrome-height + 1rem) so the
bar floats above Mana's PillNav without overlap. The CSS var comes
from <main>'s style attribute and cascades even into fixed
descendants.
- backdrop-filter: blur(10px) + theme-specific semi-transparent
background so the bar feels aerial, not docked.
- Custom CSS tooltips on every button (data-tip attribute + ::after
pseudo). Replaces the native `title` attribute which has a ~1s delay
and inherits OS chrome. Tooltip bubble colors adapt to the active
reader theme. aria-label stays for screen-readers.
- Active-state swatches get an outline-ring instead of a background-
swap so the chip color stays visible as a theme-preview.
Spacing:
- meta-bar margin-top: 1.5rem → 4rem — clearer separation between the
viewport edge and the article title.
- ReaderView padding-bottom: 4rem → 14rem — last paragraph no longer
visually attaches to the floating bar when scrolled to the end;
there's a proper "you've reached the end" gap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supersedes per-space-vs-user-global-tags.md (which recommended defer
under "ship fast" assumptions). Pre-live + unlimited resources changes
the calculus: build the clean architecture now.
Decision: tags, tag-groups, workbench scenes, AI agents, and AI
missions all become Space-scoped. Only identity (user, session,
profile, MK key) and per-device UI prefs stay user-level.
Plan covers 8 phases across ~3–4 days:
1. Audit + schema design
2. Dexie migration (with backfill to user's Personal-Space)
3. Store APIs (implicit via scopedForModule wrapper)
4. Space-switch side-effects (reset active scene, bootstrap defaults)
5. Space-creation seeding (one-shot copy tags from Personal)
6. Backend (mana-sync + Postgres + RLS)
7. Docs + memory updates
8. Delete the old deferred plan
Includes edge cases, success criteria, and reasoning for why β over γ
(two clear levels beat one recursive primitive for user clarity).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strategic decision doc covering whether the central tag system
(@mana/shared-stores → globalTags) should move from user-global to
per-Space, prompted by integration debt between Spaces (hard tenancy)
and Scene-Scope (tag-based view filter).
Surveyed current state: no spaceId column on globalTags or any of the
19 junction tables, 68 consumer imports, plaintext sync, guest-mode
seed.
Evaluated three options:
- A — status quo (user-global, no migration)
- B — fully per-Space (clean, but loses follow-me-everywhere)
- C — hybrid (nullable spaceId, recommended target if migration)
Recommendation: defer. Stay on A until one of five trigger signals
fires (first shared-Space tagging, user-reported clutter, scope
mismatch bug, >50 tags, or encryption/compliance need). Phase-by-phase
work breakdown included for when we revisit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three intertwined improvements so the "save an article" flow actually
works on real-world sites, not just bloggy happy-path URLs.
=== Consent-wall detection ===
apps/api/src/modules/articles/routes.ts: the /extract response now
includes `warning: 'probable_consent_wall'` when the extracted text
is both short (<300 words) AND contains cookie-dialog vocabulary
(Cookies zustimmen / cookie consent / Zustimmung / accept all cookies
/ enable javascript / privacy center / Datenschutzeinstellungen). The
server still returns whatever it got so the client can decide; it just
flags it as probably-not-the-article.
Frontend surfaces that warning prominently instead of silently
persisting a "Cookies zustimmen…" blob as the article body.
=== Browser-HTML extract path ===
Server-side: new POST /api/v1/articles/extract/html endpoint accepting
{ url, html }, running @mana/shared-rss's extractFromHtml on the
caller-supplied HTML. 10 MiB payload cap. Same response shape as
/extract, including the consent-wall warning (in case the bookmarklet
fires before the user dismisses the dialog).
Client-side: new extractFromHtml() in api.ts with the same 25s
timeout + typed network-error mapping as extractArticle.
AddUrlForm gains a postMessage handshake: when loaded with
?source=bookmarklet, it posts `mana-ready` to window.opener and
listens one-shot for `mana-html` with { url, html, title } from the
opener's tab. The HTML goes straight to our own /extract/html
endpoint — same-origin, carries the user's auth cookie. No CORS, no
form-submission CSP tango, no cross-origin token smuggling. If
nothing arrives within 30s we surface a clear error instead of
hanging.
Settings page adds a second "browser-HTML" bookmarklet (marked as
"Empfohlen") alongside the legacy URL bookmarklet. New snippet opens
/articles/add?source=bookmarklet in a new tab, waits for mana-ready,
then postMessages the tab's documentElement.outerHTML over. 15s
safety timeout.
This bypasses cookie-consent walls and soft paywalls because the
HTML already comes from the user's own authenticated, consented
browser tab.
=== Auto-save after successful extract ===
Previously every save path had a two-click UX: preview → confirm.
Now on clean extract the preview skips straight to persist + navigate
to the reader. Consent-wall warning is the only fallback that pauses
the flow — the user gets a "Trotzdem speichern" button to opt into
saving a teaser anyway.
Button in the manual input row is renamed "Vorschau abrufen" → "Speichern"
since it's now the commit action, not the inspect action. Loading-block
messaging distinguishes "Server extrahiert…" vs "Speichere in deine
Leseliste… Gleich weiter zum Reader."
Net click count:
Bookmarklet v1/v2 on working site: 2 clicks → 1 click
Manual paste: 2 clicks → 1 click
Consent-wall fallback: 2 clicks (explicit "Trotzdem")
Duplicate: 2 clicks ("Zum gespeicherten
Artikel")
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace raw white-alpha Tailwind utilities across the last 12 module
ListViews that were flagged by validate-theme-tokens: citycorners,
guides, inventory, memoro, picture, plants, playground, presi,
questions, times, uload, who. Also replace semantic color hex/names
(bg-yellow-500/20, bg-green-400, text-blue-400, bg-teal-600, etc.)
with success/warning/error/primary tokens.
Per-deck brand colors in who/ListView (#a855f7 purple/historical,
#ec4899 pink/women, #f59e0b amber/antiquity, #0ea5e9 blue/inventors)
stay as hex — those are domain semantics, not theme intent.
Wire validate:theme-tokens into validate:all so future regressions
fail the local pre-push gate. All 76 module ListViews now pass.
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>
User feedback after the first batch shipped: the scene-picker got
cluttered when every admin/settings subpage became its own card.
Revise the plan to codify the sharper rule instead:
- Cards are for daily workflows
- Power-user domains get ONE card with internal tabs (initialTab prop
for route deep-links)
- Config/settings stay as routes opened from the parent module's ⚙
Document the tabbed-card pattern (lib/modules/admin/tabs/*Tab.svelte
+ ListView container with role guard + initialTab), rewrite the
backlog around this principle, and fold batches 2/3/4 into a single
consolidated history that makes the scope revision explicit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: four separate admin cards (admin-users, admin-system,
admin-user-data + admin) bloated the scene-picker without adding value
— they're one logical power-user surface split four ways. Fuse them
into a single admin card with an internal tab switcher.
- lib/modules/admin/tabs/{Overview,Users,System,UserData}Tab.svelte —
each tab owns its own data + styles
- lib/modules/admin/ListView.svelte is now a tabbed container: one
role-guard, one pill-row, deep-linkable via `initialTab` prop
- /admin, /admin/users, /admin/system, /admin/user-data routes pass
the corresponding initialTab so direct URLs still land on the right
section
- Delete lib/modules/admin-{users,system,user-data}/ + three
registerApp entries
- Complexity stays a separate card (different shape — iframe-heavy,
was already its own card before this batch)
Smoketest: all 5 /admin/* routes respond 200; type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: per-module settings/preferences as separate workbench
cards bloats the scene-picker with rarely-used configuration surfaces.
Cards are for daily workflows; one-time config belongs in routes that
open from the parent module's ⚙ button.
- Inline the ListView content back into each /settings route
- Delete lib/modules/{broadcast-settings,invoices-settings,uload-settings,news-preferences}/
- Remove the four registerApp entries
Kept: spaces card (operative member management, daily use).
Deferred: admin-* cards will fuse into a single admin card with tabs
in a follow-up commit, since merging 4 power-user surfaces into tabs
is a different shape than deleting settings cards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Back-navigating from an article detail view to the list and into the
same article again crashed with
TypeError: Cannot read properties of null (reading 'removeEventListener')
Sequence that triggered it:
1. ReaderView unmounts, its own $effect cleanup calls onscroller(null).
2. DetailView sets readerScroller = null.
3. HighlightLayer's prop `scroller` becomes null.
4. The old $effect's teardown fires and reads `scroller` — which now
points at null instead of the element it had attached listeners to.
5. null.removeEventListener(...) throws, Svelte can't finish tearing
down the tree, and the re-mount never happens.
Fix: snapshot the element reference at setup time so the teardown uses
the same element the setup used, regardless of what the reactive prop
is currently pointing at. Comment block in the file explains the trap
so a future cleanup doesn't re-introduce it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that every /admin/* page is a thin wrapper over a workbench card,
the layout's nav tabs are redundant with the workbench's own scene
navigation. The heading + tab strip were also duplicating chrome that
each card now owns.
- Layout shrinks to an auth guard: redirect non-admins, gate-screen if
the session is not yet initialized.
- /admin/+page.svelte now wraps the existing admin module ListView
instead of duplicating its stats/security/quick-links grid.
Smoketested: all 11 /admin/* and settings routes respond 200 with
clean SSR output; type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useStats() live-query aggregates total / per-status / savedThisWeek /
finishedThisWeek / topSites / totalHighlights in one scoped Dexie pass.
useAllHighlights() joins cross-article highlights with article-header
info (title, siteName, originalUrl) for rendering.
/articles/highlights — HighlightsView groups chronologically-sorted
highlights per article with color-accented stripes, click-to-reader
jumps, and two export actions:
- Copy as Markdown (clipboard)
- Download .md (file)
Export logic lives in lib/markdown-export.ts as a pure function
(renderHighlightsMarkdown) so future snapshot tests don't need the
render tree.
Dashboard widget: ArticlesUnreadWidget mirrors NewsUnreadWidget's
pattern — self-contained live query, top-3 unread/reading, stats
strip ("N ungelesen · M diese Woche gespeichert"), empty state
CTA to /articles/add. Registered in:
- lib/types/dashboard.ts (WidgetType union + WIDGET_REGISTRY)
- lib/components/dashboard/widget-registry.ts (component map)
- lib/i18n/locales/dashboard/{de,en}.json (translations)
fr/it/es intentionally left untranslated — consistent with how
invoices_open and broadcasts are handled.
ListView gains a pencil button next to the settings gear linking
to /articles/highlights.
Also: plan doc marks M7 + M8 done with commit refs; M1–M8 scope is
now complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mana/shared-pwa gains PWAShareTarget + PWAShareTargetParams types
plus ManifestConfig.share_target pass-through. createPWAConfig now
accepts an optional `shareTarget` and threads it into the generated
manifest. Other apps keep working unchanged — the field is omitted
unless set.
Web app wiring:
- vite.config.ts passes shareTarget: { action: '/articles/add',
method: 'GET', params: { title, text, url } } so the installed PWA
shows up as a destination in the Android / Chromium share sheet.
- AddUrlForm reads ?url / ?text / ?title in onMount; falls back to
the first URL-shaped token in ?text because some senders (Chrome
Android, WhatsApp) put the shared link there instead of ?url. When
a URL is pre-filled the Readability preview auto-triggers, so the
user just hits "In Leseliste speichern" to confirm.
- New /articles/settings route hosts the bookmarklet (drag-to-
bookmarks-bar button + copy-to-clipboard + expandable snippet
viewer) and a short Share-Target explainer with an iOS-Safari
caveat. Linked from the ListView via a new gear button next to
"+ Neu speichern".
Bookmarklet form (origin-prefixed so it works across tenants):
javascript:void(window.open('${origin}/articles/add?url='+…))
Not in scope (plan marked optional): _pendingUrls offline queue.
Share without internet shows the existing error + retry state today;
can slot in as M7b if users hit it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the user's preference (cards over subroutes), the migration
pattern (module ListView + registerApp + thin route wrapper), what's
already shipped in batches 1 + 2, and the remaining backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shared-types/src/index.ts re-exports with explicit .ts extensions
(Tailwind v4 module resolver needs them). TS 5.7 requires consumers
to opt in via allowImportingTsExtensions. The flag only type-checks
when noEmit:true; the NestJS builder also needs
rewriteRelativeImportExtensions so tsc still emits valid JS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert 8 admin/settings subroutes into scene-droppable workbench cards
so users can arrange them alongside other modules instead of navigating
to dedicated subroutes.
Admin cards (admin-role-gated inline, fallback gate-screen for non-admins):
- admin-users: user search + paginated table
- admin-system: service-health grid + monitoring links + env info
- admin-user-data: API-backed user browser (detail route stays)
- admin-complexity: route now wraps the existing complexity card
Module-settings cards (wrap existing form components where available):
- broadcast-settings, invoices-settings: wrap SettingsForm / SenderProfileForm
- uload-settings: data-stats + JSON export + clear-local-data danger zone
- news-preferences: topics/languages/weights/onboarding reset
All 8 subroutes reduced to 10-line ListView wrappers; admin layout
keeps the role guard so the routes are still gated on direct access.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract member management from /spaces/members into a reusable
workbench-card ListView so users can drop the surface into any scene.
- lib/modules/spaces/ListView.svelte — hint + invite + members + pending
invitations, all theme-token driven
- APP_ICONS.spaces icon (three-silhouette cluster, teal→indigo)
- MANA_APPS entry id=spaces (beta tier, shared-space management)
- registerApp({ id: 'spaces' }) so the card is scene-droppable
- /spaces/+page.svelte as the new canonical route wrapper
- /spaces/members/+page.svelte kept as legacy alias
- SpaceSwitcher menu now links to /spaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five new entries in AI_TOOL_CATALOG (shared-ai/src/tools/schemas.ts):
list_articles auto Read-only listing with status +
query filter. Default hides
archived; 'all' includes them.
save_article propose URL → Readability → encrypted save.
Delegates to articlesStore.saveFromUrl
which already handles scope-aware
dedupe. Duplicates surface as
success:true with duplicate:true.
archive_article propose setStatus('archived') after
scoped existence check.
tag_article propose Case-insensitive dedupe over
globalTags; tagMutations.createTag
fills in when missing. Junction
write via articleTagOps.addTag.
add_article_highlight propose Snaps to the first verbatim
occurrence of `text` in the
decrypted article.content. Fails
cleanly when the snippet isn't
found — no orphan highlights.
Policy, client executor, and server planner derive automatically from
the catalog (see root CLAUDE.md §"AI Tool Catalog") so no manual
registration in policy.ts / services/mana-ai is needed.
Skipped from the M6 plan: <AiProposalInbox module="articles" />. The
component doesn't exist in the current codebase — after the
pendingProposals-table drop in Dexie v29 the inbox surface moved to
the mission-detail cross-module view, and articles proposals show up
there automatically. Documented in docs/plans/articles-module.md.
Also updated: plan doc now marks M1–M6 as DONE with commit refs and
the next-step pointer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes surfaced by the end-to-end smoke test.
1. broadcast-track.ts: inner route paths double-prefixed.
Routes were declared as '/track/open/:token' etc, then
mounted at '/api/v1/track', yielding '/api/v1/track/track/open/:token'
— every tracking endpoint returned 404. Dropped the redundant
'/track/' prefix so the full path is now
'/api/v1/track/{open,click,unsubscribe}/:token' as the
orchestrator + client both expect.
Verified with live curl:
- /track/open/BAD → 200 image/gif 42 bytes (graceful no-signal)
- /track/click/?url missing → 400 missing url
- /track/click?url=javascript: → 400 bad url
- /track/click?url=https://ok + bad token → 302 graceful
- /track/unsubscribe/BAD GET → 400 HTML
- /track/unsubscribe/BAD POST → 400 (RFC 8058)
2. shared-branding/tsconfig.json: allowImportingTsExtensions
missing. shared-types/src/index.ts uses explicit .ts
imports (intentional, for Tailwind's module resolver); any
downstream tsconfig without allowImportingTsExtensions emits
8 errors. shared-auth already had this fix — shared-branding
gets the same treatment. noEmit:true is set, so no rewrite
flag needed.
Verified: shared-branding pnpm check → 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M4 — Tags + Filter:
- queries.ts: useArticleTagIds(id) + batched useArticleTagMap(ids)
live queries against articleTagOps (the junction into globalTags).
- DetailView: TagField from @mana/shared-ui with the global tag pool +
this article's selected ids; onChange fans out through
articleTagOps.setTags, which diffs add/remove internally.
- ListView: 6 filter chips (Alle | Ungelesen | In Arbeit | Gelesen |
Favoriten | Archiv) with live counts. Archived articles are hidden
from the "Alle" view and only surface under the Archiv filter. Tag
chips render inline on each card using the batched tag map + the
global tag pool for colour lookup.
M5 — Migration + news deprecation:
- modules/articles/migrations/from-news.ts: boot-gated migration (per-
device localStorage sentinel). Reads newsArticles with type='saved',
decrypts under the newsArticles allowlist, re-encrypts under the
articles allowlist, and copies into the articles table. Status maps
isArchived→archived, isRead→finished, else unread. Source rows get
soft-deleted so the sync engine removes them from other devices.
Ran after crypto init (from (app)/+layout.svelte boot block), not
in the Dexie .upgrade() hook, because the decrypt→re-encrypt round-
trip needs Web Crypto + the master key.
- news/stores/articles.svelte.ts: removed saveFromUrl — ad-hoc URL
saves now live in the articles module.
- news/api.ts: removed extractFromUrl helper + ExtractedArticleDto.
The /api/v1/news/extract/* routes stay in apps/api for now because
news-research still hits them for RSS discovery.
- news/index.ts: dropped the extractFromUrl re-export.
- news/tools.ts: the save_news_article AI tool keeps its name (so
historic Mission iterations in the DB still resolve) but its
execute body now routes through the articles module's saveFromUrl.
- routes/(app)/news/add + /news/saved: replaced with single-shot
redirects to /articles/add and /articles respectively.
- news-research ListView + page: "Speichern" buttons now route to
the articles module and navigate to /articles/[id] on success.
Plan: docs/plans/articles-module.md. M6 (AI tools + proposal inbox),
M7 (share target + bookmarklet), M8 (highlights view + stats) open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>