Electronic Claims Manager: Monorepo
  • JavaScript 70.1%
  • TypeScript 22.8%
  • PLpgSQL 5.4%
  • omnetpp-msg 0.7%
  • Shell 0.5%
  • Other 0.4%
Find a file
2026-09-27 23:02:13 -07:00
app dist: rebuild at ed9d228e 2026-09-27 23:00:49 -07:00
archive Docs: the TLS keys removed from the whole history (git filter-repo, 826 commits rewritten) — commit map for the runbook IDs in archive/commit-map-2026-09-24-tls-rewrite.md; runbook and CLAUDE.md notes on the second rewrite 2026-09-24 20:15:30 -07:00
daemon ECM 4.1.0: packages, README, CHANGELOG and runbooks 2026-09-27 21:17:05 -07:00
database Canadian postal codes in the address block 2026-09-27 18:40:45 -07:00
doc Contracts list: download every contract as .xlsx or .csv 2026-09-27 22:49:33 -07:00
etc/ecm Add pgbouncer to detach DB from app 2026-05-08 14:08:37 -07:00
service Track the root package.json, its locks and service/app/README.org 2026-09-27 23:02:13 -07:00
test Contract picker: suggest the contracts with the most claims first 2026-09-27 23:00:42 -07:00
.containerignore Image: no database schema inside — .containerignore keeps the reference dump and the history migration folders (2026-09-20, 2026-09-23) out; the image carries database/js and the migrations this version requires (2026-09-24) 2026-09-24 19:36:59 -07:00
.gitignore Time log and invoices live in doc/pomo/ (gitignored) instead of ../ecm/doc/pomo; CLAUDE.md and AGENTS.md point there 2026-09-24 13:55:48 -07:00
AGENTS.md Contracts list: download every contract as .xlsx or .csv 2026-09-27 22:49:33 -07:00
CHANGELOG.md Track the root package.json, its locks and service/app/README.org 2026-09-27 23:02:13 -07:00
CLAUDE.md CLAUDE.md: invoicing time is not billable 2026-09-27 21:54:42 -07:00
LICENSE Licence: PolyForm Noncommercial 1.0.0, commercial licences on request 2026-09-27 15:01:16 -07:00
Makefile Makefiles: make dev / push-dev / inspect / promote (service/app) and root spa / test / release wrappers; dev refuses a dirty tree or a dist/version.json that does not match HEAD 2026-09-22 23:16:00 -07:00
package.json Track the root package.json, its locks and service/app/README.org 2026-09-27 23:02:13 -07:00
pnpm-lock.yaml Track the root package.json, its locks and service/app/README.org 2026-09-27 23:02:13 -07:00
PRODUCTION.md ECM 4.1.0: packages, README, CHANGELOG and runbooks 2026-09-27 21:17:05 -07:00
README.org ECM 4.1.0: packages, README, CHANGELOG and runbooks 2026-09-27 21:17:05 -07:00
skills-lock.json Track the root package.json, its locks and service/app/README.org 2026-09-27 23:02:13 -07:00
STAGING.md ECM 4.1.0: packages, README, CHANGELOG and runbooks 2026-09-27 21:17:05 -07:00

ECM — Electronic Claims Manager (monorepo)

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 .msg and internet .eml shown 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 standalone ecm-app repository it was once a subtree of was deleted on 24 Sep 2026, with the pre-rewrite history it held. service/pgbouncer is a git subtree of edoburu/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 *.fasl stay out.
  • History was rewritten on 23 Sep 2026 to drop .vorg.db and on 24 Sep 2026 to drop the TLS keys; commit IDs in runbooks from before each are old IDs, mapped in archive/commit-map-2026-09-23-vorg-rewrite.md and archive/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.md for 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 the CHANGE: backdoor, insured, risk, transactions, MI).
  • service/azure/README.org — how the production host was set up.