- JavaScript 70.1%
- TypeScript 22.8%
- PLpgSQL 5.4%
- omnetpp-msg 0.7%
- Shell 0.5%
- Other 0.4%
| app | ||
| archive | ||
| daemon | ||
| database | ||
| doc | ||
| etc/ecm | ||
| service | ||
| test | ||
| .containerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| Makefile | ||
| package.json | ||
| pnpm-lock.yaml | ||
| PRODUCTION.md | ||
| README.org | ||
| skills-lock.json | ||
| STAGING.md | ||
ECM — Electronic Claims Manager (monorepo)
- Layout
- How a request flows
- What the SPA covers
- Development
- Releasing
- Conventions
- Licence
- Where to read next
ECM is Maxwell Claims' claims-management system: claims, policies, contracts,
risks, people, accounting, diaries, time recording, bordereaux and reports for
the insurers (syndicates, agencies) whose claims it handles. This repository
holds everything that runs it: the PostgreSQL schema that carries most of the
business logic, the Bun/Hono daemon that is both the JSON API and the web
gateway, the Lit/TypeScript single-page application, the Cypress end-to-end
suite, and the container build that ships the whole thing as one image. The two
legacy web applications (Common Lisp, 2006–, and Gerbil Scheme) were ported to
the SPA and retired on 24 Sep 2026; the git tag legacy-lisp-gerbil-final
marks the last commit that had their source.
Version 4.0 (git tag v4.0, 24 Sep 2026) is the first release with the
whole ECM in the SPA, the daemon and the database: no legacy application
runs, and the image holds only Caddy and the Bun daemon. Version 4.0.1
(v4.0.1, 25 Sep 2026) adds the user guide behind a Help button and lets
administrators move a claim to another risk on its own policy. Version 4.0.2
(v4.0.2, 25 Sep 2026) shows what is left of a contract's loss fund, with its
indemnity total, on the contract page, the claim page and the transaction form. Version 4.0.3
(v4.0.3, 26 Sep 2026) corrects the bordereau figures (no negative reserves;
recoveries and incurred as on the claim page), writes the bordereau .xlsx in
Accounting format with totals, and makes carrier templates uploadable and
selectable. Version 4.0.4 (v4.0.4, 26 Sep 2026) gives every open claim a
limitation date with its "Time to look over the claim" diary, lets
administrators delete a claim with no timecards, transactions or open diaries,
shares a claim group's leader's attachments with its members (one copy of each
file; "This claim only?" keeps one off), and moves the London broker claim
number to the claim, with the lineage, as its Risk Claim #'s. Version 4.1.0
(v4.1.0, 27 Sep 2026) copies a claim group leader's transactions to its
members at their subscription % (each member keeping its own cheque details),
lets administrators approve cheques from the Transactions table and keeps a
closed claim's approvals, makes the loss its own record shared by a claim
group, moves every address of people and losses into one address table
(countries and provinces as codes, city and Canadian postal code lists,
near-duplicate warnings and merging), shows a claim's History across
everything on it, and floats a form's Save and Cancel while they are off
screen. It is the first release under the PolyForm Noncommercial licence.
Releases are
tagged v<major>.<minor>, or v<major>.<minor>.<patch> for a smaller one;
app/spa/package.json and daemon/package.json carry the same version.
CHANGELOG.md lists what each release changed.
Every directory with its own concerns has an AGENTS.md with the working
rules for that part; read the nearest one before changing anything. This file
is the map.
Layout
| Path | What it is |
|---|---|
app/spa/ |
The modern UI (@aufin/ecm-spa): Lit 3 web components, TypeScript, Vite 8, Tailwind 4, TanStack tables, Quill for rich text, SheetJS/ExcelJS for spreadsheets, File Viewer for Word/Excel previews. Server-side API code lives beside each component in api.ts and is imported by the daemon. dist/ is tracked and served by the daemon. |
daemon/ |
Bun + Hono on :3000. Serves /api/* as the logged-in user's PostgreSQL role, the SPA pages and static files, and redirects the legacy URLs into the SPA (nothing is proxied any more). Parses Outlook .msg and internet .eml attachments for the email preview. Caddy in front only terminates TLS. |
database/ |
PostgreSQL 16: ~740 functions, ~110 triggers, row-level security, one login role per user. sql/schema-2026-09-23--22-03-PCT.sql is the sanitised reference dump; sql/migrations/<dump date>/ holds the hand-applied migrations, filed under the dump that contains them. js/ is the small pg wrapper (@aufin/ecm-database); doc/ the design notes. |
test/ |
Cypress 15 end-to-end suite against a live ECM (24 specs / 142 tests). make test for the dev container, bin/test-staging.sh for staging. |
service/ |
Containerfiles and deployment: app/Containerfile builds the single application image (Caddy + the daemon + the SPA build) from the repo root (app/Makefile behind the root make targets); azure/ the production host (rootless Podman Quadlet unit ecm-app pulling docker.io/aufin/ecm:latest); database/, pgbouncer/, compose/ the supporting services. |
etc/, app/etc/ |
Runtime configuration (database endpoints, Caddyfile). Sensitive. TLS certificates and keys are neither in git nor in the image: the dev container uses local copies in app/etc/, the hosts mount their own /srv/ecm/app/etc. |
archive/ |
Superseded runbooks and invoiced change logs, kept for the record, and the commit maps of the 23 and 24 Sep 2026 history rewrites. |
app/bin/ |
ecm-appd.sh (the image's process manager: Caddy and the daemon) and ecm-live.sh (the development container). |
Runbooks at the root: PRODUCTION.md (what production runs, what is pending,
the rollback targets), STAGING.md (the staging copy, the current candidate
and its acceptance checklist, and the record of earlier releases),
CHANGELOG.md (what changed, release by release; updated with every commit).
The billing documents — Recent-Changes.md with the billable hours since the
last invoice, the time log and the invoices — live in doc/pomo/, which is
gitignored. CLAUDE.md collects the
everyday commands and the traps met so far.
How a request flows
Browser → Caddy (TLS) → Bun daemon :3000
├─ /api/* → app/spa/**/api.ts, run as the user's DB role → PostgreSQL
├─ SPA routes, static → app/spa/dist (index.html for every SPA route)
├─ legacy URLs → 302 into the SPA (/ecm/view?claim=N → /claim/N, /old/…, …)
└─ anything else → 404
Caddy and the daemon are started by app/bin/ecm-appd.sh, the image's CMD; podman stop makes it stop every service cleanly before the container exits.
Authentication. login.login_user() issues a session id kept in the
ecm-login cookie; login.token_user_id() maps it to an app_user and its
PostgreSQL role. The daemon opens a connection as that role for every
request, so authorisation is grants and row-level security in the database —
never JavaScript. A read-only user cannot write; a user restricted to certain
contracts sees only those contracts' claims, in every list, search and report.
Where the logic is. Business rules are triggers and functions in PostgreSQL
(a claim's loss date must fall inside its policy term, a policy's insured needs
an e-mail and a phone, a timecard must record time, claims cannot be deleted,
contract numbers are generated, history is kept in history.hstore_history
and every change in claim_movement). The API layers are thin: whitelisted
columns, parameterised SQL, the database's own error messages passed through
as 400s.
What the SPA covers
Everything a user does is in the SPA; the legacy applications it replaced are retired, and their old addresses redirect to the matching SPA page.
- Records: person, policy, risk, contract, claim (with clone, groups, the four tabs — transactions, timecards grouped by interim, attachments, diary), diary entries, transactions, timecards, attachments, interims (Activity Summary, create/edit), users and examiners, access lists and ACLs; a contracts list; every record's change history; deleting records (administrators).
- Attachments: multi-file upload with de-duplication, and an inline preview
of what the browser can show — images, PDF, text, audio and video; Word and
Excel rendered in the browser by File Viewer; Outlook
.msgand internet.emlshown as an email (headers, the HTML body with its inline images, its own attachments opening in a modal viewer over the message). - Create-from-anywhere: autocompletes offer "Create new …", the create form opens prefilled, and Save or Cancel returns to the form the user left with its draft intact and the new record picked — a claim can be opened from nothing without navigating by hand.
- Diary page: every user's (or everyone's) pending entries, filtered by the schedule chips — Overdue, Today, Tomorrow, Week's Time, Later — with defer, done and bulk actions.
- Message board: threaded, collapsible, rich-text posts (Quill) with inline images and attached files, hoisting, moving between boards, bug and feature threads with a status, private mail, per-board rights.
- Bordereau (
/bordereau): one page over the database's layout configuration — 19 layouts (the database ones plus all the field lists the Lisp used), for a contract, syndicate, agency or claim over a period; sortable table,.xlsx=/.csv=, optional carrier template workbooks, run history; administrators create and edit layouts on screen (a layout whose SQL does not plan cannot be saved). - Reports (
/report/<name>): one runner for every SQL-backed report — the form comes from the report's parameters, results go to the same table and downloads; the MI returns fill their carrier templates. Twenty reports (incl. Complaints and Weekly hours); all the legacy report pages redirect here. Administrators also have a read-only SQL query page (/query). - Cross-cutting: dark scheme, date paste into any date field, search with recent searches, a reload prompt when the server runs a newer build, resizable table columns remembered per table, section search boxes on the claim's tabs, printable pages.
Development
The dev container ecm-live runs the same image as production with app/
and daemon/ bind-mounted, against the database container ecm-db (:5432).
app/bin/ecm-live.sh recreate # or start|stop|status|shell; recreate after an image/mount change
# https://localhost:8432 (Caddy), http://localhost:3000 (daemon)
make spa # app/spa: tsc (the only type gate) + vite build → dist/
cd app/spa && pnpm dev # Vite on :5173, proxied to the container
# the daemon has no hot reload: after changing daemon/src/* or any api.ts, restart bun
podman exec ecm-live sh -c 'for p in $(pgrep -x bun); do kill $p; done'
podman exec ecm-live sh -c 'cd /srv/ecm/daemon && (ECM_TEST_API=1 nohup pnpm bun run src/index.ts > /srv/ecm/var/log/ecm-appd/bun.log 2>&1 &)'
CYPRESS_BASE_URL=https://localhost:8432 make test # the whole suite, ~4.5 min
podman exec ecm-db psql -U ecm -d maxclaims -Atc "…" # read-only checks
Set CYPRESS_BASE_URL explicitly: a shell that exports it for staging sends
make test there. Use --browser firefox on a single spec for selection,
focus and shadow-DOM work.
Tests never shell out or touch the database directly: they use the product
API and the daemon's test API (/api/test/*, mounted only with
ECM_TEST_API=1, administrators only, every delete fenced by a Cypress
marker). Every test row carries such a marker; sample files (.msg, .eml,
Word, Excel) are synthetic, written by the generators in
test/cypress/fixtures/.
Releasing
commit source → make spa, commit dist/ alone → local suite → make dev push-dev inspect → staging: migrations, swap to the :dev digest, test-staging.sh, hand checks → make promote (:dev → :latest, production's tag) → production: migrations first, then pull + restart as user ecm → verify from outside
make spa && git add app/spa/dist && git commit -m "dist: rebuild at <sha>"
CYPRESS_BASE_URL=https://localhost:8432 make test
make dev push-dev inspect # build :dev from HEAD, push, pull back and check offline
test/bin/test-staging.sh <admin> <password> # after the swap; check /version.json first
make promote # asks you to type yes
make dev refuses a dist/version.json stamped before the SPA sources last
changed, and uncommitted changes to tracked files. make inspect prints the
image ID, the revision label, the manifest digest, the bundled
/version.json and the migrations in the image, and fails on any TLS
material. Those go into the candidate section of STAGING.md; staging and
production swap to the pushed digest. The daemon's node_modules are copied
into the image as they are in the checkout, so run pnpm install in
daemon/ after pulling a dependency change.
The build stamps the git commit into the SPA bundle and dist/version.json;
open tabs on an older build are asked to reload. The image carries a
release's migrations at /srv/ecm/database/sql/migrations/, so they can be
applied from the host that runs the container as well as from a checkout.
PRODUCTION.md always states what production runs, what is pending and what
to roll back to.
Conventions
- Identify which generation owns a feature before changing it; preserve
legacy behaviour unless the task changes it. Prefer removing traffic from
the legacy apps (SPA route + redirect in
daemon/src/proxy.ts) to adding to them. - Enforce rules in the database, not in the UI; the UI only explains and
disables. Every person/contract/policy/claim/risk shown by name links to its
viewer. Every colour class has a
dark:twin. app/is ordinary monorepo code: the standaloneecm-apprepository it was once a subtree of was deleted on 24 Sep 2026, with the pre-rewrite history it held.service/pgbounceris agit subtreeofedoburu/docker-pgbouncer.- Never commit an unsanitised schema dump (personal-name roles renamed, no
role passwords), credentials, TLS keys, cookies or the contents of
etc/. Editor backups (*~,#…#) and*.faslstay out. - History was rewritten on 23 Sep 2026 to drop
.vorg.dband on 24 Sep 2026 to drop the TLS keys; commit IDs in runbooks from before each are old IDs, mapped inarchive/commit-map-2026-09-23-vorg-rewrite.mdandarchive/commit-map-2026-09-24-tls-rewrite.md. - Report which checks actually ran.
Licence
Copyright 2006-2026 Drew Crampsie. The ECM is source-available: free for
non-commercial use under the PolyForm Noncommercial License 1.0.0 (LICENSE).
Personal use, study, research and use by charities, educational institutions,
public bodies and other non-commercial organisations are covered. It is not
"open source" in the OSI sense, since commercial use is not granted.
Commercial use (running it for a business, or offering it to others as a product or service) needs a commercial licence. Write to Drew Crampsie at me@drewc.ca.
Releases up to v4.0.4 were published under the MIT licence, and anyone
who received them keeps those rights for those versions.
app/spa/packages/@spinal/shadow-css/ stays under the MIT licence
(its own LICENSE).
Third-party code keeps its own licence: service/pgbouncer/ (MIT,
edoburu/docker-pgbouncer), Font Awesome Free
(app/spa/src/components/fontawesome/LICENSE.txt), and the npm dependencies
under their own terms (File Viewer is Apache-2.0). The place names and
Canadian postal codes in database/data/geonames/ (the address forms' lists) are from
GeoNames, under CC BY 4.0.
Where to read next
AGENTS.md(root) — the one-page working guide;app/spa/AGENTS.md,daemon/AGENTS.md,database/AGENTS.md,test/AGENTS.mdfor each part.CLAUDE.md— commands, the release sequence and the traps.database/sql/migrations/<date>/README.md— what each migration does and in which order.database/doc/*.org— design notes (users vs roles, immutability and theCHANGE:backdoor, insured, risk, transactions, MI).service/azure/README.org— how the production host was set up.