AkurAI Build
Menu

Public repository / 3af23b0e937520000e1aa43c7e2eb9de2675abd1

AkurAI-Mail / docs/framework-migration.md

Latest change 3af23b0e937520000e1aa43c7e2eb9de2675abd1 - OLI-248: webmail list and reader as framework islands on a signals store by Ólafur Búi Ólafsson

# Webmail UI → AkurAI-Framework migration plan (OLI-241)

Framework client runtime (AkurAI-Framework `site/frontend`): native ES modules, **no build step, no npm deps**.
`signals.js` (signal/effect + `data-*` directives), `islands.js` (`registerIsland`, hydrate server-rendered markup),
`spa.js` (router), `akurai-client.js` (typed auto-API client), `ai/components` (chat/tool UI), server-side templates.
Source of truth checked: AkurAI-Framework@d506ed5.

## Inventory and mapping

| Current (SvelteKit) | Size | Framework equivalent | Migration step |
|---|---|---|---|
| `src/lib/webmail/shortcuts.ts` + toggle in `+page.svelte` | 36 | `signals.js` + plain ESM | **Done in OLI-241** → `src/framework/shortcuts.js` |
| `filters.ts`, `folders.ts`, `threads.ts`, `virtual.ts`, `recipients.ts`, `compose.ts`, `session.ts` (pure logic + tests) | ~380 | Plain ESM + JSDoc, tests stay on `bun test` | Rename `.ts`→`.js` with JSDoc types; no framework API needed. Wave 1. |
| `sync.ts`, `sync.svelte.ts` (rune store) | 91 | `signals.js` `signal`/`effect` | Wave 1 (replace runes with signals) |
| `SignalBadge`, `SyncBanner`, `SettingsDrawer` | 231 | Islands (`registerIsland`) over server-rendered markup | Wave 2 |
| `RecipientCombobox`, `RichTextEditor`, `ComposeDialog` | 212 | Islands; editor needs a framework rich-text component (gap G4) | Wave 3 |
| `MessageList.svelte` (virtual list, threads, chips) | 605 | Island + `data-for`; virtualization needs gap G3 | Wave 3 |
| `MessageReader.svelte` | 381 | Island; sanitized-HTML render | Wave 3 |
| `src/routes/webmail/+page.svelte` (state, actions, layout, CSS) | 1727 | Server template + `spa.js` route + one top-level island holding a signal store | Wave 4, last (decompose first) |
| `src/routes/webmail/+page.ts` (auth gate + load) | 18 | Framework auth/sessions (`/docs/auth`) + `akurai-client.js` | Wave 4 |
| `src/lib/api.ts` | 23 | `akurai-client.js` | Wave 4; keep `api.test.ts` green by porting 1:1 |

Order rationale: leaf logic first (no UI risk), then components bottom-up, page last. Each wave ships behind green `bun test ./src`
and `scripts/webmail-shortcuts-e2e.ts`-style browser checks.

## Wave 0 (this issue): shortcuts slice

- `src/framework/shortcuts.js`: `resolveKey` keymap, `createShortcuts` (signal-driven listener + persisted toggle), `scrollSelectedIntoView`.
- `src/framework/vendor/signals.js`: vendored framework runtime (see G1/G2).
- `+page.svelte` now only maps actions to its own state; the keymap, toggle state and scroll helper live in the framework module.
- Tests: `src/framework/shortcuts.test.js` (ported from OLI-239, written red first), plus `scripts/webmail-shortcuts-e2e.ts` (headless chromium against a mock API: Enter opens, `#` prompts delete, `j` scrolls selection into view, toggle persists and silences keys).
- Behaviour note: the OLI-239 `shortcuts.ts` was only partially wired (page had its own handler with extra keys R/f/s/u/!/Delete/arrows). The framework module is now the single keymap and covers all of them; behaviour is unchanged.

## Framework gaps (to file as AkurAI-Framework issues)

| # | Gap | Why mail needs it | Proposed owner |
|---|---|---|---|
| G1 | `signals.js` touches `document` at import time → cannot be imported under `bun test`/SSR | Unit-testing any framework module; vendored copy carries a one-line guard | Framework dev (Dev Lead to file) |
| G2 | No installable/versioned client package (files are served from the site only) | We vendor-copy `signals.js`, which can drift; need a pinned import path (npm tarball or `/vendor/akurai@x.y.z/`) | Platform Engineer + Framework |
| G3 | No virtualized list / windowing primitive for `data-for` | Inbox lists reach thousands of rows (`virtual.ts` today) | Framework dev |
| G4 | No rich-text editor component | Compose (`RichTextEditor.svelte`) | Framework dev |
| G5 | No keyboard-shortcut/hotkey helper or focus-scope utility | Generic version of today's `src/framework/shortcuts.js`; upstream it | Mail Dev (extract) → Framework |
| G6 | `data-*` directives use `new Function`/`with`; mail renders untrusted content so only island JS (not directives) may touch message data | CSP and XSS posture for message bodies | SecOps Engineer review |
| G7 | No typed (`.d.ts`) surface for signals/islands/client; we use JSDoc + `checkJs` | TypeScript-checked consumers | Framework dev |
| G8 | Framework SPA router vs. IdP return_to redirect flow (`loginUrl`) undocumented | Page auth gate migration (Wave 4) | Framework dev + IDP owner |

## Risks

- Tooling: `package.json` still pins SvelteKit/Vite; the Svelte page imports the framework module through Vite until Wave 4 removes Svelte.
- `bun` is resolved via mise `latest` — pin before wave 1 (Dev Lead).
- Do not deploy mid-migration waves without the browser check passing.

## Wave 2 progress (OLI-247)

- `src/framework/signal-badge.js`: SignalBadge view model (label/color/icon/age); `SignalBadge.svelte` now uses it.
- `src/framework/sync-state.js`: signals-based sync store with injectable clock/timers (tested); not yet wired — `sync.svelte.ts` stays until the page moves off Svelte (Wave 4 needs a signal→rune bridge).
- Not done: SyncBanner/SettingsDrawer islands (need server-rendered markup + island host from Wave 4), `svelte-check` (no node_modules in worktree).
- Run tests with the mise bun directly: `~/.local/share/mise/installs/bun/1.4.2/bin/bun test ./src` (shim fails in sandbox).

## Wave 3 progress (OLI-248): list and reader as islands

The webmail page now exists twice: the SvelteKit page (still what `bun run build` ships) and the
framework page under `app/webmail/` + `src/framework/`, assembled by `bun run build:framework`
into `build/` with no bundler (the browser loads the same ES modules `bun test` runs).

- Wave 1 finished on the way: `src/lib/webmail/*.ts` are `.js` with JSDoc types (checkJs clean).
- `src/framework/store.js`: all page state and actions on `signals.js`; fetch, storage, navigation
  and timers are injected, so `store.test.js` covers loading, cache fallback, filters, paging,
  reading, reply/forward, send + undo, send later, draft recovery and settings without a DOM.
- `src/framework/dom.js` (`h()`), `api.js`, `icons.js` (Lucide path data inlined, no runtime
  dependency; kebab or PascalCase names), vendored `islands.js` and `rich-text.js` (G2 still open).
- Islands (`src/framework/webmail.js` registers them, `app/webmail/index.html` marks them):
  `webmail-rail`, `sync-banner`, `webmail-topbar`, `mail-canvas`, `message-list`
  (`list-island.js`: folder tree with remembered collapse state, chips, smart mailboxes, windowed
  thread rows via `virtual.js`, pager), `message-reader` (`reader-island.js`: toolbar, move menu,
  signals panel, body, attachments, empty states) and `webmail-overlays` (toasts, delete
  confirmation, shortcut reference). `compose-dialog` and `settings-drawer` are registered as
  empty mount points for wave 4.
- Gotchas: `islands.js` hydrates on import, so `webmail.js` calls `hydrate()` again after
  registering; islands mount once the `ready` store promise resolves. Browser imports need
  explicit `.js` extensions. A new selection scrolls into view from inside the list render, not a
  separate effect (effects re-subscribe in a different order after they re-run).
- `scripts/webmail-e2e.ts` (`bun run e2e:framework`): mock API + headless chromium, 19 checks
  (windowing, folders, Enter/j/k, archive, delete dialog, search, folder switch, chips, help).
- Not in wave 3: compose, settings drawer, admin pages, login, SvelteKit removal (wave 4, OLI-249).