Electronic Claims Manager: Monorepo
  • JavaScript 57.6%
  • TypeScript 27.9%
  • PLpgSQL 12.7%
  • Shell 0.9%
  • omnetpp-msg 0.4%
  • Other 0.3%
Find a file
2026-11-12 14:50:48 -07:00
app dist: rebuild at e11f562d 2026-11-12 14:48:28 -07:00
archive Docs: the MIT history is the private repository ECM/ecm-legacy, no longer a branch here 2026-10-20 23:19:39 -07:00
bin Tools: the backup retries a piece that fails to send; production backed up before 4.7.3 2026-11-06 20:13:08 -07:00
daemon Docs: named 4.7.10, announced (message 722) 2026-11-12 14:48:22 -07:00
database Database: reference schema of 11 Nov (4.7.9), its migrations filed; staging cleaned up 2026-11-11 18:06:11 -07:00
doc SPA, tests: a mailed cheque's Cheque cell says MAIL and where it goes; follow files' System code asked from the start 2026-11-12 13:57:22 -07:00
etc/ecm Licence: PolyForm Noncommercial 1.0.0, commercial licences on request 2026-09-27 15:01:16 -07:00
service Service: production moved to the new VM through Cloudflare (6 Nov, checked) 2026-11-06 15:21:01 -07:00
test SPA, tests: a mailed cheque's Cheque cell says MAIL and where it goes; follow files' System code asked from the start 2026-11-12 13:57:22 -07:00
.containerignore Database: reference schema of 11 Nov (4.7.9), its migrations filed; staging cleaned up 2026-11-11 18:06:11 -07:00
.gitignore make runbook: the runbooks' current steps as web pages with Copy buttons 2026-09-29 16:13:59 -07:00
AGENTS.md SPA, daemon, database, tests: System codes kept by administrators (Manage → System codes), the dropdown offering the shown ones 2026-11-11 16:06:31 -07:00
CHANGELOG.md Docs: 4.7.10's announcement posted again (message 723) 2026-11-12 14:50:48 -07:00
CLAUDE.md Docs: threads 691 and 702 moved to Feature request and closed; how to close a General thread 2026-11-12 14:47:26 -07:00
LICENSE Licence: PolyForm Noncommercial 1.0.0, commercial licences on request 2026-09-27 15:01:16 -07:00
Makefile make runbook: the runbooks' current steps as web pages with Copy buttons 2026-09-29 16:13:59 -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 Docs: 4.7.10's announcement posted again (message 723) 2026-11-12 14:50:48 -07:00
README.org Docs: named 4.7.10, announced (message 722) 2026-11-12 14:48:22 -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 Docs: 4.7.10 live on production (12 Nov), Marisa answered (720, 721) 2026-11-12 14:41:57 -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.

The current version is 4.7.10 (v4.7.10, 12 Nov 2026). The SPA, the daemon and the database carry the whole ECM: no legacy application runs, and the image holds only Caddy and the Bun daemon. 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 every release and what it 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-10-16-14-51-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 (35 specs / 213 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. Every sign-in goes through Better Auth in the daemon (daemon/src/auth.js, nothing outside the ECM): the password is still checked by PostgreSQL, optionally followed by a code from an authenticator app; a passkey signs in with the device's PIN, fingerprint or face and no password; Microsoft or Google sign-in is ready for when it is configured. However it began, it ends as an ordinary session id 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. The commercial subscription agreement's template is doc/licence/ECM-Commercial-Subscription-Licence-Template.md (CAD $3,000 a month, net 30, with monthly hours of support, administration or development; adjusted yearly with the CPI); bin/licence-fill.mjs fills it in for one Licensee and prints it, ready to sign.

Releases up to v4.0.4 were published under the MIT licence, and anyone who received them keeps those rights for those versions. This repository begins with the PolyForm licence (27 September 2026); the earlier history is kept privately in ECM/ecm-legacy.

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.md — how the production host is set up (setup-app-host.sh), and the move to a new VM.