Module internals
What the platform does under the hood when it loads, scopes, and runs a module.
How every module that ships today is actually built, and how tightly each is coupled to platform internals. This is the empirical companion to architecture.md: it shows where the "fine line" between module and platform currently sits, module by module, so migrations onto the unified contract can be prioritized.
settings.self), Image Converter (campaign UI + Pillow worker + bundled art), Word Editor (a self-contained .docx engine served directly as the module UI), Excel Editor (the OfficeEditorPanel grid + ribbon, saving IN PLACE via a new scoped office capability), Field Ops Planner (the crew planner reaching its own backend routes through a generic module-request capability), and Certificate Builder (the PowerPoint certificate generator — streaming per-certificate progress + an in-canvas PDF.js preview) — most authored as Svelte and compiled with the bundler (npm run module:build), served through the generic module host from bundled_modules/. No first-party built-in panels remain. The per-module sections below describe the pre-migration built-in implementations (the migration source); the shipped shape is manifest + bundled iframe UI + optional worker. | Packaged ✅ | Built-in panel — to migrate | |---|---| | Calculator, Simple Excel, Calendar, Image Converter, Word Editor, Excel Editor, Field Ops Planner, Certificate Builder | — (all migrated) |How a built-in launches today #
- User opens a module →
openApp(catalogItem)creates a window; sizing comes frommanifest.ui.window. - The window body renders via a hardcoded
{#if appId === '…'}chain that maps eachmodule_idto a specific Svelte component. - Most modules call the backend through
$lib/api.ts— either the generic job contractPOST /runtime/modules/{id}/jobs, bespoke runtime routes, or VFS file routes.
Generic jobs run inline (synchronously) inside the request via create_first_party_module_job. As of Phase 3, dispatch goes through the worker registry (register_worker / get_worker) rather than a if job.module_id == … switch; each worker receives a WorkerContext. Built-ins still register their existing generators verbatim — migrating them to the scoped context is Phase 4.
Coupling legend: tight = imports platform services directly / bespoke routes; moderate = uses VFS + storage helpers but self-contained logic; minimal = client-only or read-only VFS.
Certificate Builder — coupling: tight · ✅ now packaged #
bundled_modules/certificate-builder/: the multi-step wizard (CertificateBuilderPanel) + a PDF.js CertificatePdfPreview ported onto bridge shims. It needed a streaming sibling of HANABI_MODULE_REQUEST (the SDK's moduleStream, commit d9e51ed) so the generate-stream NDJSON progress reaches the sandbox one certificate at a time. The PDF preview can't use the browser viewer — Chrome blocks a PDF inside an opaque sandboxed iframe — so the bytes are read over the bridge and rendered to a <canvas> by PDF.js, whose worker is bundled inline as a blob (a worker-src … blob: CSP addition lets it run; object-src 'none' keeps <embed>/<object> blocked). Downloads are client-built zips (readFile → store-zip → saveBlob); job history moved from localStorage to settings.self. The win32com PowerPoint → PDF automation and the four bespoke routes stay UNCHANGED and privileged server-side — only the UI moved into the sandbox. This was the canonical "trusted-worker" case and the last built-in to migrate. The pre-migration built-in is described below as the source.- UI:
CertificateBuilderPanel.svelte(multi-step: upload → review → configure → generate → done) +CertificatePdfPreview.svelte. - Backend:
certificate_builder.py. - Routes: bespoke, in
runtime.py:/preview,/generate-one,/generate-stream(NDJSON progress),/generate-combined; plus the generic job path. - VFS: reads inventory
.xlsx/.xlsmfrom Imports; writes PDFs into dated subfolders underModules/Certificate Builder/Exports. - Deps:
openpyxl(parse),python-pptx/manual PPTX XML, `pywin32` + `pythoncom` (PowerPoint COM → PDF). Windows + Office only. - Process: sync for preview/one; NDJSON stream for batch (one PowerPoint session); job path for bulk.
- Migration note (done): the canonical "trusted-worker" module — the PowerPoint COM worker stays privileged server-side, the UI moved into the sandbox, and the bespoke routes are reached through the streaming
moduleStreamcapability rather than folded away.
Image Converter — coupling: moderate · ✅ now packaged #
bundled_modules/image-converter/ — the campaign UI (CampaignBattle + $lib/campaign) and its 2.4 MB of art bundled into a single self-contained ui/; platform calls rewired to the capability bridge (writeFile/createJob/readFile/notify/openFolder); the Pillow worker unchanged. The art ships under ui/campaign-art with a relative path (the module's own origin). The job-history view was dropped (no module-capability equivalent for listJobs). The pre-migration built-in is described below as the migration source.- UI:
ImageConverterPanel.svelte(removed when packaged) — a gamified converter: it wrapped batch conversion in an RPG "campaign/battle" metaphor (CampaignBattle.svelte+$lib/campaign). Clearing stages converted images. The packaged UI now ships underbundled_modules/image-converter/. - Backend:
image_converter.py. - Routes: generic
POST /runtime/modules/image-converter/jobsviacreateRuntimeModuleJob. - VFS: reads source images (16 formats) from selection/Imports; writes PNG/JPG to
Modules/Image Converter/Exports(optional subfolder). - Deps:
Pillow; optionalpillow-heif(HEIC),rawpy(RAW/DNG). - Process: job-based; bounded by a CPU semaphore; commits the DB row before the slow decode to release the SQLite write lock (keeps the app responsive).
- Migration note: good early "trusted-worker" migration — logic is self-contained; the campaign UI is pure front-end.
Simple Excel Sheet — coupling: tight · ✅ now packaged #
bundled_modules/simple-excel/ — Svelte UI compiled to a bundled ui/index.html and served through the module host; the worker is registered for the simple-excel job. The pre-migration built-in is described below as the migration source.- UI:
SimpleExcelPanel.svelte(removed when packaged) (upload inventory → populate template → download); nowbundled_modules/simple-excel/. - Backend:
simple_excel_builder.py. - Routes: generic job path (
module_id = "simple-excel"). - VFS: reads source
.xlsx/.xlsm+ a bundled template; writes a merged.xlsxtoModules/Simple Excel Sheet/Exports. - Deps:
openpyxl. - Process: job-based, synchronous; reuses/overwrites a single named output.
- Migration note: template lives in
assets/templates/; a packaged version would ship the template as aconfigfolder asset.
Word / Excel Editor (Office Editor) — coupling: moderate · Word + Excel ✅ packaged #
bundled_modules/word-editor/: the self-contained .docx engine (ooxml.js / editor.js / export.js) serves directly as the module UI, its inline host script rewired to the capability bridge (read the launch openedFile, write the export to Exports). No Syncfusion — that manifest dep was stale. Excel Editor migrated too at bundled_modules/excel-editor/: the whole OfficeEditorPanel grid + ribbon ported into a self-contained Svelte module, driven by the launched openedFile and saving in place via new scoped readOffice/saveOffice bridge capabilities (HANABI_READ_OFFICE / HANABI_SAVE_OFFICE) that proxy to the same openpyxl /files/{id}/office routes — the first packaged module that writes back to the file it was opened with, gated to that launch file (or the module's own write-scope via canWriteFile). The built-in office-editor panel stays as the fallback for spreadsheets when the module isn't installed (and still handles the COM-only legacy .xls path the module drops).- UI:
OfficeEditorPanel.svelte(spreadsheets/CSV) andWordEditorPanel.svelte(removed when packaged — now [`bundled_modules/word-editor/`](../../services/api/hanabi_api/bundled_modules/word-editor/)) (documents). - Backend:
office_editor.py. - Routes: VFS file routes —
GET/PATCH /files/{file_id}/office(spreadsheets & documents),GET/PATCH /files/{file_id}/text+/text/save-as(text). - VFS: reads/writes the same file in place across the VFS (any
.xlsx/.xlsm/.xls/.csv/.docx/.doc). - Deps:
openpyxl;pywin32COM fallback for legacy.xls/.doc. - Process: synchronous per file. Word Editor is the one working iframe + postMessage precedent: it hosts
/word-editor/index.html(a SvelteKit static asset) in an iframe and bridges with a bespokehanabi-word-*protocol. This is the prototype the generic host/bridge generalizes.
Field Ops Planner — coupling: bespoke · ✅ now packaged #
bundled_modules/field-ops-planner/: the FieldOpsPlannerPanel + 35 views and the client store ported into a self-contained Svelte module with its own .fieldops-root-scoped theme (no host-theme dependency). The "module-owned routes" blocker the migration note below predicted is solved by a generic `HANABI_MODULE_REQUEST` bridge capability: the module reaches its own /runtime/modules/field-ops-planner/* backend (GET/PUT /state, POST /parse, /parse-report, /reset) through the host instead of calling /runtime directly, which the opaque-origin sandbox cannot do. The backend router and field_ops/* services are unchanged; state stays a per-user JSON blob proxied through that capability. The pre-migration built-in is described below as the migration source.- UI:
FieldOpsPlannerPanel.svelte+field-ops/views; rich client store in$lib/field-ops. - Backend:
services/field_ops/—state,connecteam,msg,seed,ai_cleanup. - Routes: its own router
field_ops.py, prefix/runtime/modules/field-ops-planner:GET/PUT /state,POST /parse,/parse-report,/reset. - State: the whole workspace is one JSON blob per user in the
field_ops_statetable (not VFS files) — debounced client saves. - VFS: reads Connecteam
.xlsxand.msgreports from Imports; no permanent VFS outputs. - Deps:
openpyxl,emailparser; optional AI note-cleanup (Featherless/Google). No Office COM. - Migration note (done): was the most platform-coupled. The "place for module-specific routes in the unified model" turned out to be the
HANABI_MODULE_REQUESTcapability, which proxies a module to its own/runtime/modules/<id>/*routes (reserved pathsjobs/app/kv/media-ticketblocked); the "module-owned state document" is justGET/PUT /stateover that proxy.
Media Player — coupling: minimal #
- UI:
MediaPlayerPanel.svelte+media.ts+audio-mixer.ts. - Backend: none — plays VFS files directly via inline/preview URLs.
- Deps: none (HTML5
<video>/<audio>/<img>). - Migration note: trivially expressible as a sandboxed UI-only module reading via
files.read.
Campaign Battle — coupling: minimal (not a standalone module) #
- A client-side game engine (
CampaignBattle.svelte+$lib/campaign, removed when Image Converter was packaged) embedded inside Image Converter as its UI metaphor. Read the user's avatar as the hero sprite; saves were client-side zips. No backend, no routes.
Summary #
| Module | UI component | Backend service | Routes | Heavy deps | Process | Coupling |
|---|---|---|---|---|---|---|
| Certificate Builder | CertificateBuilderPanel | certificate_builder.py | bespoke /preview,/generate-* + job | openpyxl, pptx, pywin32 | sync + NDJSON + job | tight |
| Image Converter | ImageConverterPanel (+CampaignBattle) | image_converter.py | generic job | Pillow, pillow-heif, rawpy | job | moderate |
| Simple Excel | SimpleExcelPanel | simple_excel_builder.py | generic job | openpyxl | job | tight |
| Office Editor | OfficeEditorPanel / WordEditorPanel | office_editor.py | /files/{id}/office,/text | openpyxl, pywin32 | sync (Word: iframe+bridge) | moderate |
| Field Ops Planner | FieldOpsPlannerPanel + field-ops/* | field_ops/* | own /field-ops-planner/* router | openpyxl, email, AI | sync + debounced state | bespoke |
| Media Player | MediaPlayerPanel | — | VFS file serving | — | client | minimal |
| Campaign Battle | CampaignBattle | — | — | — | client | (embedded) |
What this means for unification #
- Minimal (Media Player) and client-only UIs migrate first — they prove the sandboxed-UI rail with little risk.
- Moderate (Image Converter, Office spreadsheets) migrate next — their logic is self-contained; wrap it as a registered worker.
- Tight / bespoke (legacy Office COM) validated the "trusted-worker" and "module-owned state / module-specific capability" parts of the contract last. Both bespoke cases are now packaged: Field Ops Planner rides the
HANABI_MODULE_REQUESTcapability for its module-specific routes, and Certificate Builder — the last built-in — keeps its privileged PowerPoint COM worker server-side while its UI drives the streamingmoduleStreamcapability forgenerate-streamand PDF.js rendering to a<canvas>for the PDF preview (Chrome blocks its built-in viewer in the sandbox). All eight first-party modules are migrated; no built-in panels remain.
The recurring pattern removed: direct imports of `job_service` / `vfs_service` / `FileRecord` and bespoke per-module routes. Replacing those with the worker registry + capability bridge is what turned each built-in into a self-contained module.