Menu
Public repository / 054e5cda0171bd13facff3c16a38f2048a1052cc
AkurAI-Mail / docs/framework-migration.md
Latest change 054e5cda0171bd13facff3c16a38f2048a1052cc - OLI-249: compose, settings, admin console and sign-in on the framework; SvelteKit removed 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).
## Wave 4 (OLI-249): compose, settings, admin, login — SvelteKit removed
- `bun run build` now runs `scripts/build.ts`: `app/` + `static/` + `src/framework` + `src/lib/webmail`
(no tests) are copied into `build/`, unchanged. No bundler, no Svelte, no Vite; devDependencies are
`typescript` (`bun run check` = `tsc --noEmit`, checkJs), `@types/*` and `lucide-svelte` (only read by
`scripts/gen-icons.ts`). akurai-mail-api serves `build/` exactly as before (AkurAI-MailAPI deploy.sh).
- `compose-island.js` (recipient comboboxes from `recipient-combobox.js`, the vendored framework
`rich-text` island plus a quote button, attachments, send / send later / draft / discard) and
`settings-island.js` (scheduled sends, account + sync status, theme, shortcuts, contacts, signature,
vacation reply, rules builder, import/export) fill the mount points left by wave 3.
- Admin console: `app/index.html`, `app/{mailboxes,domains,antispam,dns}/index.html` share
`src/framework/admin.js` (admin gate → members go to `/webmail`, data loading, `/api/actions`) and
`app/admin.css` (the pages' former style blocks, duplicates collapsed).
- `app/login/index.html` + `src/framework/login.js`: IdP button first (keeps `return_to`), mailbox and
admin password forms post to `/api/webmail/login` / `/api/login`. The build also serves it at
`/webmail/login`, where the API's mailbox logout lands. `scripts/validate.sh`'s "login page" check
passes again.
- Dropped: `src/routes/fixture-preview` (5,000-row Svelte fixture; the e2e covers windowing),
`src/routes/api/health` (the API owns `/api/health`), `src/lib/api.ts` (now `src/framework/api.js`).
- `bun run e2e` (`scripts/webmail-e2e.ts`): mock API + headless chromium over CDP, now also compose,
settings, login and every admin page.
- Open framework gaps: G2 (vendored `signals.js`, `islands.js`, `rich-text.js` carry the source
commit in their header; re-vendor by hand), G6 (message bodies render the API's sanitized HTML with
`innerHTML`, as before).