Commit graph

3113 commits

Author SHA1 Message Date
Till JS
f2faaf1387 refactor(pill-nav): drop Settings entry, guard on settingsHref
Settings lives in the Workbench now — no longer needs its own pill.
settingsHref becomes optional; both places that render the entry
(user-menu link list + nav overflow dropdown) skip it when unset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:43:29 +02:00
Till JS
f7426ab40f feat(ai): policy is read from the owning agent (Phase 4)
Until now AiPolicy lived as a user-global setting consulted for every
AI action. With agents as the principal unit of AI behavior, policy
belongs on the agent — different agents can be aggressive about tasks
but conservative about calendar edits, etc.

Webapp (tools/executor.ts):
- When an AI actor invokes a tool, the executor looks up the owning
  agent via getAgent(actor.principalId) and passes agent.policy into
  resolvePolicy. Falls back to DEFAULT_AI_POLICY when the agent record
  is missing (legacy write, deleted agent, race) so no tool call can
  silently bypass the propose/deny path.
- resolvePolicy already accepted an optional policy arg, so the call
  site change is a single line plus the agent load.

Server (mana-ai):
- ServerAgent gains an optional policy field, projected off the same
  plaintext JSONB that the webapp writes.
- Tick loop filters AI_AVAILABLE_TOOLS through filterToolsByAgentPolicy
  before passing them to the planner prompt. Resolution order mirrors
  the webapp: tools[name] → defaultsByModule → defaultForAi; 'deny'
  drops the tool so the LLM never even sees it.

Phase 5 will surface a per-agent policy editor on the agent-detail
UI. Until then all agents inherit DEFAULT_AI_POLICY (baked in during
createAgent), which means no behavior change for existing users —
every tool that was 'propose' before is still 'propose' now, just
reached via agent.policy instead of the user-level singleton.

Tests: mana-ai 41/41, webapp svelte-check clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:43:04 +02:00
Till JS
968e08059f chore(workbench): delete dead SceneTabs component
SceneTabs.svelte had no remaining importers — the homepage
switched to SceneAppBar long ago, and nothing else in the repo
referenced it (grep for imports comes back empty). It still
carried its own rendering logic, context menu, and rename-dialog
wiring, which kept drifting from the real bar (e.g. the scene
icon field removal yesterday had to touch it even though the
file is never mounted). Remove it outright; any future compact
scene tab ever needed can be re-added fresh against the current
store shape.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:34:54 +02:00
Till JS
32f4c0d10d feat(page-carousel): empty-state message when scene has no apps
When the scene has zero apps the carousel used to show just a
small "+ Hinzufügen" button centered in 60vh of whitespace —
visually underpowered and easy to miss on mobile. Replace with a
proper empty state:

- Heading: "Diese Szene ist leer"
- Hint line: "Füge eine App hinzu, um loszulegen — oder drücke [0]"
- The existing add-card button sits underneath

The kbd chip styles match the layout's typical keyboard hint
pattern and quietly teach the '0' shortcut that already opens
the picker (registered at +page.svelte keybind handler).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:34:39 +02:00
Till JS
db959b6f8f feat(workbench): auto-scroll on scene switch, unify rename to inline
Two tightly coupled UX fixes on the homepage:

1. Switching scenes via SceneAppBar used to leave the carousel
   parked wherever it was before — the new scene's SceneHeader
   appeared off-screen to the left, and the user saw a seemingly
   stale row of cards until they manually scrolled. Now an
   $effect watches activeSceneId, tracks the last seen value, and
   smooth-scrolls the .fokus-track to left=0 on every real change
   (ignoring the initial hydration tick so we don't fight the
   carousel's own centering).

2. Scene rename had two concurrent paths: the SceneHeader
   contenteditable <h1> (live DOM) and a SceneRenameDialog modal
   opened from the scene-pill context menu (reads from the store).
   If a user was mid-edit inline and right-clicked Umbenennen,
   the dialog opened with the pre-edit value and on save clobbered
   the inline change. The modal is gone. The context-menu
   "Umbenennen" entry now switches to the target scene, scrolls
   the carousel to the header, and focuses the contenteditable
   after a 120ms tick so the scroll has time to start. Single
   source of truth, single code path.

SceneRenameDialog.svelte + the sceneDialog state machine in
+page.svelte are removed. ConfirmDialog (used for delete) is
untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:34:22 +02:00
Till JS
f06ca2c7c3 feat(ai-missions): inline AiProposalInbox in mission detail (cross-module)
Mission detail now renders all pending proposals for that mission,
across every module, directly above the iteration list. No more jumping
between /todo, /news, /calendar to approve what the agent staged.

AiProposalInbox.module is now optional; when omitted, every card grows
a small lowercase module badge (e.g. "news", "todo") so the user knows
where each proposal will land on approve. The existing per-module
inboxes on /todo, /news, /calendar still work — proposals show in both
places via the same live query, so approving in one auto-clears the
other.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:33:06 +02:00
Till JS
988c17a678 feat(quiz): edit existing questions + wire up guest seed
- Pencil button on each question opens the bottom form pre-filled;
  submit updates in place instead of appending.
- Guest users now see the demo quiz on first visit (QUIZ_GUEST_SEED
  registered with seedAllGuestData).
- Silence state_referenced_locally warnings with svelte-ignore to
  match the pattern used in cards / landing / rsvp views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:24:53 +02:00
Till JS
3b99356464 feat(quiz): new Quiz module — build & play private quizzes (Phase 1)
Four question types (single/multi/truefalse/text), inline editor,
play view with per-question feedback + final score review. Attempts
are persisted per quiz. Encrypted at rest: title/description/tags on
the container, questionText/explanation/options on questions.
Attempts stay plaintext. Dexie v21, appId 'quiz', tier 'guest'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:54:07 +02:00
Till JS
0af50f0166 feat(mana-ai): agent-aware tick loop + snapshot projection (Phase 3)
Third phase of the Multi-Agent Workbench. The background mission
runner now respects the owning Agent: agent state gates whether
a mission runs, concurrency is capped per-agent, and server-produced
iterations carry the agent's identity as their Actor.

Data layer:
- db/migrate.ts: new mana_ai.agent_snapshots table (mirrors
  mission_snapshots) with indexes on (user_id, last_applied_at) and
  a partial index on active agents.
- db/agents-projection.ts: refreshAgentSnapshots (incremental LWW
  replay over sync_changes appId='ai' table='agents') +
  loadActiveAgents / loadAgent helpers. mergeRaw exported for tests.
- db/missions-projection.ts: ServerMission.agentId + projection
  reads the JSONB field (undefined for legacy missions).

Tick integration (cron/tick.ts):
- Refreshes both snapshot tables on every pass (parallel).
- Per-user in-tick agent cache (Map<userId, Map<agentId, Agent>>)
  so N missions for one user hit the DB once.
- Gate order: agent archived → skip silently; agent paused → skip;
  per-agent maxConcurrentMissions exhausted this tick → defer to next.
  All skip paths bump mana_ai_agent_decisions_total{decision}.
- Prompt injection: withAgentContext prepends an <agent_context>
  block to the system prompt with the agent's name + role, and
  plaintext systemPrompt + memory when available. Ciphertext
  (enc:1:… blobs) are skipped — server has no key by design. Mirrors
  the Mission Grant privacy stance: encrypted context belongs to the
  foreground runner.

Iteration writer (db/iteration-writer.ts):
- New optional `agent` + `iterationId` + `rationale` inputs.
- When agent is present, the sync_changes row is stamped with a
  makeAgentActor actor (principalId=agentId, displayName=agent.name)
  so the webapp timeline groups the write under the right agent.
- Falls back to an AI actor with LEGACY_AI_PRINCIPAL + 'Mana' when
  the mission has no owning agent; ultimate fallback to the
  mission-runner system actor when iterationId is also missing.

Metrics:
- mana_ai_agent_decisions_total{decision=ran|skipped-paused|
  skipped-archived|skipped-concurrency}. Missions without an agent
  don't produce this metric — plansWrittenBackTotal is the universal
  "did we run" counter.

Tests: 41/41 (was 35) including 6 new cases for the agent LWW merge.
mana-ai type-check clean. Webapp svelte-check: 0 errors (4 unrelated
warnings in a different module).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:46:57 +02:00
Till JS
8b6b73627c feat(page-carousel): five-preset width picker replaces drag handle
PageShell drops the bottom-right drag handle and its pointer/touch
tracking entirely, and adds a small header button (ArrowsOutLineHorizontal)
that opens a menu with five discrete widths: XS 340 · S 440 · M 540 ·
L 720 · XL 960. Each entry shows its label and pixel value and
highlights whichever one is currently closest to the persisted
widthPx — so legacy freehand values (e.g. 823) still light up the
nearest entry when the menu first opens.

New module width-presets.ts holds the preset array and a
nearestPresetIndex() helper so the same snapping logic isn't
duplicated between PageShell and the store.

Also drops the .page-shell.resizing style (and the `resizing`
state, the shellEl ref, and the MIN/MAX width/height constants)
that only existed to colour the shell while a drag was active.

The five presets cover the useful width range without wasting
space on tiny viewport-sized differences; anyone wanting an
uncommon width can adjust widthPx directly in Dexie for now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:37:19 +02:00
Till JS
681136266b refactor(workbench): drop per-card height and free-form resize
Height was practically unused — most WorkbenchSceneApp rows had
heightPx: undefined and scrolled internally, and the few explicit
values nobody ever revisited were just noise in the sync ledger.
Same goes for pixel-precise drag: users would produce widths like
823px and then never touch them again. Both paths come out in
favour of a fixed set of five width presets (next commit) that
guarantee cards sit on sensible sizes without a decision per card.

- types/workbench-scenes: heightPx removed from WorkbenchSceneApp
- page-carousel/types: heightPx removed from CarouselPage
- stores/workbench-scenes: resizeApp(appId, widthPx) loses its
  height param; patchScene allowlist no longer lists heightPx.
  Existing Dexie rows with heightPx set simply stop being read
  — the field ages out the next time the row is written.
- PageCarousel: placeholder renders width-only; comment updated
- AppPage: heightPx prop + onResize height param gone
- +page.svelte: carouselPages no longer carries heightPx, handler
  signature narrowed to (id, widthPx)

This commit removes the data; the next commit rewires PageShell's
UI so the drag handle is replaced by the five-preset picker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:37:03 +02:00
Till JS
bc77b36234 feat(agents): Agent CRUD + default bootstrap + Mission.agentId (Phase 2)
Second phase of the Multi-Agent Workbench rollout (docs/plans/
multi-agent-workbench.md). Builds on Phase 1's identity-aware Actor.

Adds the Agent primitive — a named AI persona that owns Missions,
carries its own policy + memory, and (from Phase 3 on) drives the
Workbench lens. Everything is wired; a single user currently has one
"Mana" default agent until the UI (Phase 5) lets them create more.

Shared types (@mana/shared-ai):
- agents/types.ts: Agent, AgentState, DEFAULT_AGENT_ID/NAME constants
- policy/types.ts: AiPolicy + PolicyDecision (moved from webapp so
  Agent.policy can reference it without a runtime dep on the web app)
- missions/types.ts: new optional Mission.agentId field

Webapp data layer:
- data/ai/agents/{types,store,queries,bootstrap}.ts
- Dexie schema v19 adds `agents` table (indexes on state, name,
  [state+name]); sync registered under the existing ai app-id
- Encryption registry: agents.systemPrompt + agents.memory encrypted;
  name/role/avatar/policy stay plaintext for search + UI rendering
- DuplicateAgentNameError thrown at write time (not a Dexie unique
  index — bootstrap races between tabs would otherwise hit
  ConstraintError; store now resolves via getOrCreateAgent)
- bootstrap.ts: ensureDefaultAgent + backfillMissionsAgentId. The
  backfill runs once per device (localStorage sentinel) so missions
  that pre-date the rollout get stamped with the default agent's id.
  Called fire-and-forget from startMissionTick() during layout init.

Runner threading (already merged into d5c351d63 via Till's debug-log
commit that picked up my uncommitted edits):
- runner.ts + server-iteration-staging.ts now resolve mission.agentId
  to the real Agent and build makeAgentActor with agent.name as
  displayName. Missing-agent fallback keeps using LEGACY_AI_PRINCIPAL
  so historical writes still attribute cleanly.

Tests: shared-ai 26/26, mana-ai 35/35, svelte-check 0 errors.
Agent store vitest suite is present but blocked by a pre-existing
\$lib alias resolution issue in the webapp vitest config that
predates this phase (proposals/store.test.ts is broken the same way
on HEAD). Will address separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:35:49 +02:00
Till JS
d5c351d63e feat(ai): per-iteration debug log — capture prompt + response + inputs
New local-only Dexie table _aiDebugLog (v20, never synced) holds one
row per mission iteration with the full system+user prompt, raw LLM
response, latency, every ResolvedInput the planner saw, and pre-step
state (kontext-injected? web-research-ok-or-error?). Capped at 50
newest rows.

aiPlanTask always returns the captured prompt/response on AiPlanOutput.
debug; the runner persists it only when isAiDebugEnabled() — toggled
via a checkbox in the Mission detail header (defaults to on in DEV
builds, off in prod, override via localStorage 'mana.ai.debug').

New <AiDebugBlock> component renders below each iteration card:
expandable sections for Pre-Step, Resolved Inputs (each input
individually collapsible), System Prompt, User Prompt, Raw Response,
plus a "📋 JSON" copy-to-clipboard button for bug reports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:33:17 +02:00
Till JS
6e842a83c9 fix(workbench): set SceneHeader text via refs instead of inline mustache
Prettier kept reformatting the <h1>/<p> bodies onto their own
indented lines, which contenteditable renders verbatim — the user
saw leading/trailing whitespace inside the edit buffer.
prettier-ignore only applies to the immediately next node, and the
svelte-ignore comment was taking that slot, so the directive never
reached the element.

Rewire with bind:this + two $effects that set textContent whenever
the scene prop changes and the element isn't currently focused.
Side benefit: a scene name synced from another device now updates
the header live without interrupting an in-progress edit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:16:21 +02:00
Till JS
e2ea0cd3b8 fix(workbench): tighten SceneHeader spacing and keep contenteditable clean
Two small follow-ups to the inline-edit header:

- Reduce the right padding from 2.5rem to 0.25rem so the scene name
  sits visually adjacent to the first card. With the carousel's own
  1rem gap between flex children the total breathing room is now
  ~20px instead of ~56px, which matches how hero titles usually
  relate to the cards below / beside them.
- prettier-ignore on the <h1> and <p> element blocks. Prettier's
  default Svelte formatting moved {scene.name} onto its own line
  with indentation whitespace, which contenteditable renders
  verbatim as literal leading / trailing spaces in the editor
  content. Keep the mustache on the same line as the tag so the
  text value is what the store actually holds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:14:31 +02:00
Till JS
1771063df4 refactor(actor): identity-aware Actor for Multi-Agent Workbench (Phase 1)
Foundation for the Multi-Agent Workbench roadmap
(docs/plans/multi-agent-workbench.md). Every event, record, and
sync_changes row now carries a principal identity + cached display
name in addition to the three-kind discriminator.

Shape change (source of truth in @mana/shared-ai):
  Before: { kind: 'user' | 'ai' | 'system', ...kind-specific fields }
  After:  discriminated union on kind, with
            - common:  principalId, displayName
            - 'user':  principalId = userId
            - 'ai':    principalId = agentId + missionId/iterationId/rationale
            - 'system': principalId = one of SYSTEM_* sentinel strings
                        ('system:projection', 'system:mission-runner', etc.)

Key design calls (from the plan's Q&A):
- System sub-sources get distinct principalIds (not a shared 'system'
  bucket) — lets Workbench filter + revert distinguish projection
  writes from migration writes from server-iteration writes
- displayName cached on the record so renaming an agent doesn't
  rewrite history
- normalizeActor() compat shim fills principalId/displayName on
  legacy rows with 'legacy:*' sentinels so historical events never
  crash the timeline

New exports:
- BaseActor / UserActor / AiActor / SystemActor (narrowed types)
- makeUserActor, makeAgentActor, makeSystemActor (factories with
  typed return)
- SYSTEM_PROJECTION, SYSTEM_RULE, SYSTEM_MIGRATION, SYSTEM_STREAM,
  SYSTEM_MISSION_RUNNER (principalId constants)
- LEGACY_USER_PRINCIPAL, LEGACY_AI_PRINCIPAL, LEGACY_SYSTEM_PRINCIPAL
- isUserActor / isFromMissionRunner predicates

Webapp:
- data/events/actor.ts now re-exports from shared-ai, keeps runtime
  ambient-context (runAs, getCurrentActor) local
- bindDefaultUser(userId, displayName) lets the auth layer replace
  the legacy placeholder with the real logged-in user actor at login
- Mission runner + server-iteration-staging stamp LEGACY_AI_PRINCIPAL
  as the agentId placeholder — Phase 2 will thread the real agent
- Streaks projection uses makeSystemActor(SYSTEM_PROJECTION)
- All test fixtures migrated to factories

Service:
- mana-ai/db/iteration-writer.ts stamps makeSystemActor(
  SYSTEM_MISSION_RUNNER) instead of the old { kind:'system',
  source:'mission-runner' } shape. Phase 3 will switch this to an
  agent actor per mission.

Tests: 26 shared-ai + 21 webapp vitest + 35 mana-ai — all green.
svelte-check: 0 errors, 0 warnings.

No behavior change; purely a type + shape upgrade. Old sync_changes
rows parse via the normalizeActor compat shim at read time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:13:57 +02:00
Till JS
f7b5c9b3a4 feat(workbench): inline-edit the scene header, no modal
SceneHeader switches from a button-that-opens-a-dialog to inline
contenteditable fields. Click the title → caret appears, type →
Enter commits, Escape reverts, blur commits. Same for the
description except Enter is allowed (multi-line).

- contenteditable="plaintext-only" keeps pasted rich text from
  leaking styles into the store
- onfocus selects the existing text so the first keystroke
  replaces rather than appends (matches the expected rename feel)
- :empty-based ::before placeholder shows "Beschreibung
  hinzufügen…" on blank descriptions, including while focused
  (slightly dimmer) so the edit target is still visible before
  the first character
- Empty name on blur reverts to the previous value (empty scene
  names aren't a valid state the rest of the UI handles)
- Empty description commits as null, matching the store's
  setSceneDescription contract

handleEditActiveScene and the SceneHeader.onEdit prop are gone —
the dialog is no longer reachable from the header. It stays
hooked up to SceneTabs' right-click → Umbenennen path for users
who prefer the modal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:13:41 +02:00
Till JS
db8e681120 feat(workbench): render scene header left of the first page
New SceneHeader component: big scene name (clamp 2.75rem–4.5rem
responsive) plus a muted description underneath, or an italic
"Beschreibung hinzufügen…" placeholder when empty. The whole block
is a button — clicking it opens the existing scene edit dialog,
now pulling double duty for both name and description.

Wired through PageCarousel's new leading snippet from the previous
commit, so the header scrolls with the track and stays anchored to
the visual start of the carousel without needing a second scroll
container.

SceneRenameDialog grows a description textarea (maxlength 240,
3 rows, vertically resizable) and onSubmit now passes (name,
description). The caller translates an empty description to null
so the DB column reflects "no description set" rather than an
empty string — keeps WorkbenchScene.description truthy checks
honest.

handleEditActiveScene resolves the currently-active scene and
opens the dialog pre-filled; used by the SceneHeader click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:42:20 +02:00
Till JS
8f3ffefdf1 feat(page-carousel): optional leading snippet before first page
Consumers of PageCarousel can now pass a \`leading\` Snippet that
renders as the first flex child inside .fokus-track, ahead of the
page wrappers. Used on the workbench homepage for the scene header
(name + description). Scrolls with the track rather than sticking
in place — reads as an intro block, not app chrome, and doesn't
steal viewport from the cards on narrow screens.

Styled as flex-aligned, align-self:stretch so its intrinsic layout
decides the height and it centres vertically against the cards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:42:04 +02:00
Till JS
714c235798 feat(workbench): scene description field, drop scene icon
Schema + store preparation for the new scene header on the homepage
(next commit). Scenes get an optional free-text description stamped
as its own field — LWW through the existing mana-sync pipeline, no
new sync contract. The unused icon field is removed everywhere:

- types/workbench-scenes.ts: description?: string | null replaces icon
- stores/workbench-scenes.svelte.ts: createScene, renameScene,
  duplicateScene, toScene, patchScene all updated. New method
  setSceneDescription(id, value) mirrors renameScene so the caller
  can change just the description without re-submitting the name.
- components/workbench/scenes/SceneTabs.svelte: the tab-bar rendered
  {#if scene.icon} before the name — scene.name is unique enough to
  identify a scene, and the UI direction is away from emoji chrome.

Existing scene rows in Dexie simply omit description (undefined → null
on read); the icon field on old rows is ignored and will age out of
the schema the next time the row is written.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:41:50 +02:00
Till JS
0da74587ce fix(ai): surface web-research failures to the planner instead of hallucinating
When the pre-step research call throws (mana-search down, missing tier,
402 credits, etc.), the runner used to swallow the error and feed an
empty input to the planner — which then made up a story about a "failed
web search" and fell back to create_task. Now we inject an explicit
"research failed" ResolvedInput with the actual error message, plus
write the truncated message into phaseDetail so it's visible in the
mission card without DevTools.

Bundles an in-flight actor refactor merge in runner.ts (makeAgentActor
+ LEGACY_AI_PRINCIPAL) — those lines came from the parallel Phase-1
identity work, not this fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:23:59 +02:00
Till JS
901d450416 refactor(shared-ui): fold Cloud Sync pill into the user menu bar
The standalone "Cloud Sync" pill sat fifth in the bar-mode pill row
on mobile / narrow layouts, pushing the pill row to 5+ items and
duplicating actions the user menu already surfaces (Sync-Einstellungen,
credits link). Move the sync status + actions into userMenuBarItems
so they appear as a labelled section inside the bar that opens when
the user pill is tapped.

Render order for signed-in users now goes:
  account · settings · theme-mode · theme · [Sync section] · logout

The section header uses the existing `divider: true, label: 'Sync'`
shape PillDropdownBar already understands, so the status line ("Cloud
Sync aktiv", etc.) sits under a real heading. For guests the section
is skipped entirely — sync doesn't apply before login.

Left the non-barMode popover path alone (showSyncStatus + syncStatusItems
still drive the inline PillDropdown when a popover is in use) since
the complaint was specifically about the bar's fifth pill on mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:47 +02:00
Till JS
fdb8e60d07 feat(ai): web-research pre-step + auto-kontext + save_news_article tool
Mission objectives matching /recherch|research|news|finde|suche|aktuelle|neueste/i
trigger a synchronous deep-research call (mana-search + mana-llm via the
existing /api/v1/research/start-sync pipeline) before the planner runs;
the summary plus top-8 source URLs are injected as a synthetic ResolvedInput
so the planner can stage save_news_article proposals against real URLs.

The kontext singleton is auto-attached to every mission's planner input
(decrypted client-side, gated on non-empty content + not already linked).

save_news_article is a new proposable tool routed through articlesStore
.saveFromUrl (Readability via /api/v1/news/extract/save). AiProposalInbox
mounted on /news so the user can approve/reject inline. mana-ai planner
tool list mirrors the new tool to keep the boot-time drift guard happy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:10:13 +02:00
Till JS
4c8034f9d0 chore(dev): seed real credit balance in setup-dev-user.sh
The shared-hono credits client returns DEFAULT_BALANCE=1000 when
/api/v1/internal/credits/balance/:userId responds with no row, so
local-dev accounts silently diverge from production — credit-gated
flows look free in dev and only blow up after deploy. Seeding a
real credits.balances row makes the fallback unreachable and the
dev stack exercises the same code path as prod.

Default is 10_000 credits (overridable via CREDITS env var) and
is applied alongside the existing tier + role + sync-gift upserts,
so setup-dev-user.sh stays a single idempotent pass. Existing dev
accounts (tills95, tilljkb, rajiehq) were backfilled manually
once; re-running the script won't clobber a higher balance
because the ON CONFLICT uses GREATEST.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:51:39 +02:00
Till JS
eaf97aeebf fix(api): unblock tsc by dropping rootDir and allowing .ts imports
Running pnpm type-check inside apps/api failed before any real
error could run, blocked by two structural errors: drizzle.presi.config.ts
and scripts/generate-who-dossiers.ts are deliberately outside src/
but are matched by the include pattern, tripping TS6059 against
rootDir=src. And @mana/shared-types imports peer files with explicit
.ts extensions, which needs allowImportingTsExtensions under
moduleResolution=bundler.

Remove rootDir (we're noEmit anyway — Bun runs src/index.ts
directly, tsc is only a lint pass), drop the unused outDir, add
noEmit explicitly, and enable allowImportingTsExtensions. Type-check
now completes cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:51:26 +02:00
Till JS
53fb3232f3 chore(dev): also grant role=admin in setup-dev-user.sh
Admin-gated backend endpoints (e.g. POST /api/v1/admin/sync/:id/gift,
GET /api/v1/admin/users/:id/tier) check auth.users.role === 'admin',
which is orthogonal to access_tier. The script was already lifting
every dev account to tier=founder but left role at the 'user'
default, so founders couldn't exercise the admin UI flows against
their local stack. Wire role alongside tier (both via env-overridable
defaults) and reflect it in the success output so re-runs surface
what's being applied.

Backfilled the existing three dev accounts (tills95, tilljkb,
rajiehq) to role=admin manually once; re-running the script now is
idempotent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:18:31 +02:00
Till JS
c5a4c5158f fix(mana-crawler): default DATABASE_URL to mana_platform in dev
The Go binary's config.go hardcoded "postgresql://…/mana" as the
DATABASE_URL fallback, but no database named "mana" exists locally
or in the macmini compose stack — the platform DB is mana_platform.
Anyone running the crawler without an explicit override got a
"database \"mana\" does not exist" crash at startup. The dev:crawler
script in package.json had been papering over this by setting
DATABASE_URL explicitly; drop that override now that the binary
default is correct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:18:19 +02:00
Till JS
0ef650d94a chore(dev): run mana-credits locally and gift sync to dev users
Two halves of the same "why is sync inactive in dev" fix:

- package.json: new dev:credits script and mana-credits added to
  the dev:mana:servers concurrently group. The service was never
  started by pnpm dev:mana:all, so the frontend's
  GET /api/v1/sync/status failed, syncBilling.load() caught the
  error and defaulted to inactive — while mana-sync (Go) was
  actually fail-open on the billing check, making the UI
  indicator lie about the backend state.
- scripts/dev/setup-dev-user.sh: after the existing
  email-verify + tier-lift UPDATE, upsert a row into
  credits.sync_subscriptions with is_gifted=true. Mirrors what
  POST /api/v1/admin/sync/:id/gift would do, so every new dev
  user gets Cloud Sync from the first login without a separate
  admin call. The credits schema lives inside mana_platform, so
  no new database needed — just a second statement in the same
  psql heredoc.

Existing dev users (tills95, tilljkb, rajiehq) were backfilled
manually with the same INSERT … ON CONFLICT DO UPDATE once;
future runs of setup-dev-user.sh stay idempotent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:09:26 +02:00
Till JS
82cf190650 feat(tunnel): route mana-ai.mana.how → mana-ai:3067
Public ingress for the Mission Key-Grant audit endpoint
(/api/v1/me/ai-audit) so the Workbench "Datenzugriff" tab can reach
mana-ai from the browser. Background tick + /metrics stay internal;
only the JWT-gated user endpoint is exposed.

Requires a Cloudflare DNS record pointing mana-ai.mana.how at the
tunnel CNAME (one-off: \`cloudflared tunnel route dns
1435166a-0e3f-4222-8de6-744f32cea5c9 mana-ai.mana.how\`), then sync
via scripts/mac-mini/sync-tunnel-config.sh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:51:59 +02:00
Till JS
298bf7e857 fix(auth-ui): narrow login/register form and drop stuck skip link
Two visible issues on /login and /register in the Mana web app:

- The "Zum Anmeldeformular springen" skip link was visible in the
  top-left corner instead of only on keyboard focus. It's an a11y
  nicety that in practice clutters the page for every user. Remove
  the button (and the dead skipToForm() handler in LoginPage) rather
  than try to fix an sr-only variant that apparently doesn't survive
  the Tailwind v4 @source pipeline here.
- The form card rendered ~full-width on desktop even though the
  existing max-w-[440px] / max-w-[480px] arbitrary-value classes
  should have constrained it. Replace those with style:max-width
  inline so the 440px cap is guaranteed regardless of how Tailwind
  picks up classes from the shared-auth-ui package.

Same changes applied to RegisterPage (only width — no skip link
there) so the two pages stay visually identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:45:41 +02:00
Till JS
6b1e8e878e fix(web): guard PwaUpdatePrompt against SSR navigator access
useRegisterSW() reaches for navigator.serviceWorker at call time, which
crashes SvelteKit SSR with "ReferenceError: navigator is not defined"
(Node has no navigator). The prod mana-web container crash-looped on
every request after the rebuild because the layout mounts this
component unconditionally.

Fix: branch on \$app/environment's \`browser\` flag. On the server,
hand back a noop writable + async-noop updater so the downstream
template code stays unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:33:35 +02:00
Till JS
87eabcde3e style(shared-ui): remove TagStrip edge fade gradients
The 2rem linear-gradient mask at each end of the tag strip was
supposed to hint at scrollable overflow, but it cuts off the
leading + trailing tags on narrow layouts where they're already
just barely visible. Dropping both mask-image declarations lets
the pills render edge-to-edge. Overflow scrolling + hidden
scrollbar stay as they were, plus the hover-lift removal from
the previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:32:28 +02:00
Till JS
fffd3eb75a style(shared-ui): drop hover-lift transforms from pills and chips
The whole pill family used a translateY(-1px / -2px) on :hover that
made the chrome jitter every time the cursor crossed anything —
especially noticeable in the TagStrip, PillNav, and the user-menu
bar where pills sit close together and a 1-2px jump reads as
twitching rather than polish.

Removed the hover transform (and the matching :active reset on
the base Pill) from:

- Pill.svelte — base primitive, covers PillNav + every bar pill
- AppDrawer.svelte — .glass-pill in the app switcher
- PillTagSelector.svelte — .glass-pill in the quick-input tag selector
- PillTimeRangeSelector.svelte — .glass-pill in the calendar range picker
- UserMenuPanel.svelte — .chip hover in the popover variant

Background, border, and shadow hover states are kept — only the
vertical displacement is gone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:32:18 +02:00
Till JS
39c85ccdcc feat(shared-ui): add "Anmelden" entry to the guest user-menu bar
When the right-hand user pill is clicked as a guest (barMode), the
opening bar used to show just a decorative "Menü" label pill on
the left and then settings + theme toggles — none of which are a
useful first action for someone not logged in.

- phosphor-icon-map: register SignIn as `login`, so PillDropdownBar
  can render it the same way as the existing `logout` glyph.
- PillNavigation.userMenuBarItems: prepend an "Anmelden" item for
  guests (no userEmail + loginHref given) that navigates to
  loginHref. It becomes the first CTA in the bar, above settings /
  theme / language.
- PillNavigation.userBarConfig: drop the bar-header label when the
  viewer is a guest. The decorative "Menü" pill added nothing
  alongside the new Anmelden action and just cluttered the leading
  edge of the bar.

The popover variant of the user menu (UserMenuPanel) already had
its own guest login button — this mirrors that behaviour for the
bottom-bar variant used on mobile / narrow layouts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:31:53 +02:00
Till JS
98347cfd89 fix(web): add workbox-window as explicit devDependency
vite-plugin-pwa's \`virtual:pwa-register/svelte\` imports workbox-window
at build time. The package was resolved transitively via @vite-pwa/
sveltekit but not installed in the webapp's own node_modules when
pnpm fetches only the workspace deps in a restricted Docker context.
Result: Rollup 'failed to resolve import "workbox-window"' at build.

Pinning the direct dep so the Docker build picks it up in the
filtered pnpm install.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:12:33 +02:00
Till JS
eb30d49501 fix(web-docker): copy packages/shared-ai into build context
Webapp package.json gained a @mana/shared-ai workspace dep (Mission
Grant types + canonical HKDF derivation). Without the package in the
Dockerfile COPY list, pnpm install aborts with
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND. Caught during the first mana-web
rebuild after the Mission Grant rollout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:07:41 +02:00
Till JS
572820f3d6 feat(infra): wire mana-ai URL + PUBLIC_AI_MISSION_GRANTS flag into mana-web
Webapp now passes:
- PUBLIC_MANA_AI_URL / PUBLIC_MANA_AI_URL_CLIENT → getManaAiUrl()
  resolves these; powers the Workbench "Datenzugriff" tab fetch.
- PUBLIC_AI_MISSION_GRANTS (default false) → gates the MissionGrant
  dialog + audit tab. Flip to "true" in .env once the keypair is
  provisioned.

Follow-up for operator: add a Cloudflare tunnel route for
mana-ai.mana.how → mana-ai:3067 (mirroring the existing pattern
for credits/events/llm) so the audit fetch resolves from the browser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:03:42 +02:00
Till JS
2497a65937 feat(ai-missions): richer error surfacing + retry button on failed runs
Replaces the single-line summary ("Planner failed: fetch …") with
full diagnostic detail: error name + message + last-active phase +
stack trace, all persisted onto the iteration itself. UI expands a
collapsed details block next to each failed iteration, so the user
can see *where* it broke ("TypeError in calling-llm") without opening
DevTools.

Paired with a one-click Retry button that re-runs the mission under
the same config — useful while debugging a flaky backend (GPU server
down, Gemini quota, etc.).

- `packages/shared-ai/src/missions/types.ts` — new
  `MissionIteration.errorDetails: { name, message, phase?, stack? }`
- `finishIteration` accepts the field, deep-clones it, and also now
  clears the transient phase markers (currentPhase/phaseStartedAt/
  phaseDetail/cancelRequested) whenever an iteration finalises — keeps
  the schema honest (phases are sub-state of \`running\` only).
- `runMission` tracks \`lastPhase\` via a new \`enterPhase\` helper that
  wraps setIterationPhase. The catch handler populates errorDetails
  with lastPhase + message + stack.
- ListView: \`<details>\` block under each failed iteration + Retry
  button (disabled while another run is in-flight).

77/77 webapp tests still green; svelte-check clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:37:15 +02:00
Till JS
70c62e7584 fix(infra): include shared-logger in mana-ai + mana-auth Dockerfile installers
Some checks are pending
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
CI / Build mana-media (push) Blocked by required conditions
CI / Build mana-credits (push) Blocked by required conditions
CI / Build mana-web (push) Blocked by required conditions
CI / Build chat-backend (push) Blocked by required conditions
CI / Build chat-web (push) Blocked by required conditions
CI / Build todo-backend (push) Blocked by required conditions
CI / Build todo-web (push) Blocked by required conditions
CI / Build calendar-backend (push) Blocked by required conditions
CI / Build calendar-web (push) Blocked by required conditions
CI / Build clock-web (push) Blocked by required conditions
CI / Build contacts-backend (push) Blocked by required conditions
CI / Build contacts-web (push) Blocked by required conditions
CI / Build presi-web (push) Blocked by required conditions
CI / Build storage-backend (push) Blocked by required conditions
CI / Build storage-web (push) Blocked by required conditions
CI / Build telegram-stats-bot (push) Blocked by required conditions
CI / Build food-backend (push) Blocked by required conditions
CI / Build food-web (push) Blocked by required conditions
CI / Build skilltree-web (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build quotes-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
shared-hono declares @mana/shared-logger as a workspace dep. Without
that package in the installer stage, Bun fails at runtime with ENOENT
reading /app/packages/shared-hono/node_modules/@mana/shared-logger.
Caught when mana-ai crash-looped on first boot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:34:08 +02:00
Till JS
f0f5b7dcf6 fix(infra): relocate mana-ai from 3066 to 3067 — port clash with news-ingester
news-ingester already owns 3066 (see docker-compose.macmini.yml:1620).
Moving mana-ai to 3067 — the next free slot in the 306x services block
(credits 3061, user 3062, subscriptions 3063, analytics 3064,
events 3065, news-ingester 3066, mana-ai 3067).

Updated: Dockerfile EXPOSE + HEALTHCHECK, config.ts default,
compose service/healthcheck/port mapping, webapp getManaAiUrl()
fallback, root CLAUDE.md service list, mana-ai/CLAUDE.md, and
COMPANION_BRAIN_ARCHITECTURE.md §20 file map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:32:07 +02:00
Till JS
6acb044230 feat(kontext,notes): cross-module handoff — save Kontext as a Note
Wires the "Als Notiz speichern" action at the bottom of the Kontext
widget (UI itself landed in 003f75f7e) to actually open Notes next
to Kontext and focus the new note:

- workbench-scenes: new addAppAfter(appId, anchorAppId). addApp()
  always appended, which pushed Notes to the far end of the
  carousel; addAppAfter inserts directly after the anchor (Kontext)
  and no-ops if the target is already open so the user's current
  position isn't yanked around.
- notes/stores/selection: new transient in-memory focus signal
  (focusedNoteId) that cross-module callers populate. Kept
  non-persistent intentionally — surviving a remount would re-open
  random notes after page loads.
- notes/ListView: $effect reads focusedNoteId, waits for the
  Dexie liveQuery to surface the just-created row, opens it in
  the inline editor, clears the focus signal, then scrolls the
  matching data-note-id element into view via queueMicrotask so
  the DOM has rendered the editor variant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:24:56 +02:00
Till JS
cb384bc7ef feat(infra): deploy mana-ai + wire Mission Grant keys via docker-compose
Wire the Mission Key-Grant feature into the production Mac Mini
compose stack so mana-ai can boot and mana-auth can mint grants.

- New mana-ai service block (port 3066) — 256m mem limit, depends on
  postgres + mana-llm, tick interval configurable via
  MANA_AI_TICK_INTERVAL_MS / MANA_AI_TICK_ENABLED. Pulls
  MANA_AI_PRIVATE_KEY_PEM from env; absent = grants silently disabled.
- mana-auth environment gains MANA_AI_PUBLIC_KEY_PEM (default empty
  so existing deployments without the keypair degrade to 503
  GRANT_NOT_CONFIGURED rather than failing to boot).
- mana-auth Dockerfile rewritten to the two-stage pnpm+bun pattern
  used by mana-credits/mana-events — required now that mana-auth has
  a @mana/shared-ai workspace dep. The previous single-stage
  Dockerfile with service-scoped build context couldn't resolve any
  @mana/* imports; that only worked historically because it fell
  through at runtime via a pre-built layer.
- mana-ai Dockerfile copies packages/shared-ai into the installer
  stage alongside shared-hono.

The build contexts for mana-auth flip from services/mana-auth to the
repo root. Existing CI/CD paths (scripts/mac-mini/build-app.sh) pass
through to docker compose build and pick up the new context
automatically — no script edits needed.

Flip-on procedure: on the Mac Mini, set MANA_AI_PUBLIC_KEY_PEM +
MANA_AI_PRIVATE_KEY_PEM in .env (already done, see
secrets/mana-ai/README.md on the host), then rebuild mana-auth +
build mana-ai.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:24:50 +02:00
Till JS
12072c6b6c feat(kontext): URL import helpers — API client + appendContent
Frontend plumbing for the Kontext "Aus URL" inline panel (the UI
itself was committed earlier as part of 003f75f7e):

- kontext/api.ts: new module-scoped fetch wrapper that talks to
  /api/v1/context/import-url with a Bearer token. Kept in kontext/
  rather than a shared helper because it's the only caller; moving
  it to `context/` would mix two unrelated modules again.
- kontext/stores/kontext.svelte.ts: new appendContent(chunk) method.
  The singleton row is encrypted at rest, so we decrypt the current
  content before concatenating with a '\n\n---\n\n' separator and
  writing back — going through setContent() to keep encryption +
  Dexie hook behaviour consistent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:24:38 +02:00
Till JS
121a0c0a6f feat(api): POST /api/v1/context/import-url — crawler + optional LLM summary
New backend endpoint that wraps mana-crawler + mana-llm in a single
call so the Kontext "Aus URL" UI can hit one route:

- Starts a crawl job (single page or up-to-20-page deep crawl) via
  mana-crawler's /api/v1/crawl, polls status up to 90s, then fetches
  paginated results.
- When multiple pages are returned, joins them into one markdown
  document with H1-per-page section headers separated by ---.
- When summarize=true, routes the collected markdown through
  mana-llm/chat/completions with a system prompt that asks for
  "Überblick / Kernaussagen / Details" H2 structure in the source
  language. sanitizeSummary() strips the common local-LLM artefacts
  (```markdown fences, "Hier ist …:" preamble, stray leading H1)
  so the output drops cleanly into the Kontext doc. On summary
  failure the endpoint returns 502 rather than silently falling
  back to the raw crawl.
- Credits are validated + consumed via @mana/shared-hono/credits
  (1 credit crawl-only, 5 crawl+summary) under the new
  AI_CONTEXT_IMPORT_URL action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:24:19 +02:00
Till JS
7f1520d6f4 chore(dev): wire mana-crawler into the local dev stack
Three small config changes so the Kontext "Aus URL" flow (next commit)
is runnable from a plain `pnpm dev:mana:all`:

- package.json: include mana-crawler in the dev:mana:servers
  concurrently group, and pass DATABASE_URL=…/mana_platform so the
  Go binary doesn't try to connect to a non-existent `mana` DB (its
  hardcoded default).
- .env.development: publish MANA_CRAWLER_URL=http://localhost:3023
  (the crawler's default binary port — the macmini container is
  a 3014 override, kept only in docker-compose). Also surface
  MANA_LLM_DEFAULT_MODEL for the summariser.
- docker-compose.macmini.yml: inject MANA_CRAWLER_URL + the
  default-model env into the mana-api container so production
  can reach the internal crawler and pick the summariser model
  consistently.

No runtime code touched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:23:54 +02:00
Till JS
003f75f7ef chore(web): unblock pre-push hook
Pre-push runs svelte-check --fail-on-warnings. Two items were blocking:

- +layout.svelte: drop four .pill-nav-toggle* rules that have no
  matching markup anywhere in the app (dead CSS from the pill-nav
  rework that removed the collapse toggle).
- kontext KontextView.svelte: drop the explicit `: Phase[]` annotation
  on `let importPhases = \$derived.by(...)`. Svelte 5's runes return a
  wrapped type that TypeScript reads as a function when the annotation
  is present, which is what produced the "not callable" + "state
  invalid placement" chain. Inferred type is the same Phase[] shape.

No runtime behaviour change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:21:35 +02:00
Till JS
ef47adb7d7 feat(ai-missions): live phase + elapsed + cancel for running iterations
Closes the "iteration is running, no feedback" black hole. The user now
sees, per running iteration:

    Frage Planner · frage Planner an              ⏱ 23s
                                              [Abbrechen]

Phases (\`IterationPhase\`):
  resolving-inputs → calling-llm → parsing-response →
  staging-proposals → finalizing

The runner advances through these via \`setIterationPhase\` between each
await, writing currentPhase + phaseDetail + phaseStartedAt onto the
iteration. UI reads them via Dexie liveQuery — no polling.

Cancel:
- \`requestIterationCancel\` writes cancelRequested=true on the iteration
- runner polls \`isCancelRequested\` between every phase + per stage step
- cancellation finalises as \`failed\` with summary \`'cancelled by user'\`
- UI button is disabled + relabelled "Wird abgebrochen…" until the next
  poll picks it up

Hard timeout: 90 s wall-clock per iteration via Promise.race against a
CancelledError. Wedged backends (e.g. flaky mana-llm) fail fast with
"timeout after 90s" instead of sitting in \`running\` forever.

Elapsed counter is a \$state variable ticking once a second, scoped to
the ListView component — Dexie isn't touched. Auto-cleaned on
component destroy.

shared-ai re-exports \`IterationPhase\` so server-side mana-ai can
inspect the same phase enum (no consumer there yet, but the type is
ready for the run-status endpoint planned in HEALTH page).

77/77 webapp tests still green; svelte-check clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:15:48 +02:00
Till JS
bb3da78d5c feat(ai): Mission Grant rollout gating — flag, alerts, runbook, user docs
Phase 4 — everything needed to flip the Mission Key-Grant feature on
safely per deployment. No new behaviour; purely operational plumbing.

- PUBLIC_AI_MISSION_GRANTS feature flag (default off). hooks.server.ts
  injects window.__PUBLIC_AI_MISSION_GRANTS__, api/config.ts exposes
  isMissionGrantsEnabled(). Grant UI (dialog + status box) and the
  Workbench "Datenzugriff" tab both hide when the flag is off.
- PUBLIC_MANA_AI_URL added to the injection set so the webapp can reach
  the new audit endpoint from production.
- Prometheus alerts (new mana_ai_alerts group):
  - ManaAIServiceDown (warning, 2m)
  - ManaAIGrantScopeViolation (critical, 0m) — MUST stay at 0; any
    increment pages immediately
  - ManaAIGrantSkipsHigh (warning, 15m) — flags keypair drift
  - ManaAIPlannerParseFailures (warning, 10m) — prompt/LLM drift
- Runbook in docs/plans/ai-mission-key-grant.md: initial keypair gen,
  leak-response procedure (rotate + invalidate all grants + audit),
  scope-violation triage.
- User-facing doc in apps/docs security.mdx: new "AI Mission Grants"
  section with the three hard constraints (ZK users blocked, scope
  changes invalidate cryptographically, revocation is one click) plus
  an honest threat-model comparison column showing where grants shift
  the tradeoff.

Rollout remaining (not code): generate keypair on Mac Mini, provision
MANA_AI_PRIVATE_KEY_PEM + MANA_AI_PUBLIC_KEY_PEM via Docker secrets,
flip PUBLIC_AI_MISSION_GRANTS=true starting with till-only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:02:47 +02:00
Till JS
74bbfda212 feat(ai): Mission Grant consent UI + Workbench audit tab
Phase 3 — user-facing side of the Mission Key-Grant rollout. Users
can now opt into server-side execution, revoke it, and inspect every
decrypt the runner has performed.

Webapp:
- MissionGrantDialog explains the scope (record count, tables, TTL,
  audit visibility, revocation) and calls requestMissionGrant. Error
  paths render distinctly for ZK, not-configured, missing vault.
- Mission detail shows a Server-Zugriff box with status pill
  (aktiv/abgelaufen/nicht erteilt), Neu-erteilen + Zurückziehen
  buttons. Only renders for missions with at least one encrypted-
  table input.
- store.ts: setMissionGrant / revokeMissionGrant helpers, Proxy-
  stripped like the rest of the store's writes.
- Workbench adds a Timeline/Datenzugriff tab switch. Audit tab queries
  the new GET /api/v1/me/ai-audit endpoint, renders decrypt events
  with color-coded status pills (ok/failed/scope-violation) and
  stable reason strings.
- getManaAiUrl() added to api/config for the audit fetch.

mana-ai:
- GET /api/v1/me/ai-audit (JWT-gated via shared-hono authMiddleware)
  backed by readDecryptAudit() — withUser + RLS double-gate so a user
  can only read their own rows.
- Limit capped at 1000, newest-first.

Missions without a grant continue to work exactly as before; the
grant UI is purely additive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:53:11 +02:00
Till JS
4b29f6d293 fix(ai-missions): swap structuredClone for JSON-roundtrip deepClone
Browser \`structuredClone\` itself fails on Svelte 5 \$state Proxies
("Failed to execute 'structuredClone' on 'Window': [object Array]
could not be cloned") — it doesn't transparently unwrap the Proxy the
way I'd hoped. The structured-clone algorithm refuses any non-cloneable
host object, including Svelte's reactive wrappers.

JSON.parse(JSON.stringify(...)) traverses through the Proxy by reading
each property normally, producing a plain-data copy that Dexie can
serialise without complaint.

Mission payloads are pure JSON values (strings/numbers/arrays/objects)
so JSON-roundtrip is lossless. The new \`deepClone\` helper is local to
this file with a comment pointing at the structured-clone failure.

77/77 webapp tests still green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:50:05 +02:00