Development

Local setup

Requirements: Node 22+, PostgreSQL 16.

$ git clone https://github.com/nreed97/EzFD.git
$ cd EzFD
$ npm install

$ createdb ezfd
$ psql -d ezfd -f db/schema.sql

$ cat > .env.local <<'EOF'
DATABASE_URL=postgres://localhost:5432/ezfd
EZFD_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
EOF

$ npm run dev

The encryption key above is fine locally; generate a real one with openssl rand -hex 32 for anything else.

db/schema.sql is idempotent, so re-applying it after a schema change is the normal workflow rather than a migration step.

Before finishing a change

$ npx tsc --noEmit
$ npm run build

Both must be clean. This is the gate AGENTS.md sets and CI enforces.

Tests

Four suites, all runnable locally against any database with the schema applied, all run by CI.

# The SES overlap guarantee lives in a database constraint,
# so it's asserted against a real database
$ psql -d ezfd -v ON_ERROR_STOP=1 -f db/test-ses-constraint.sql

# Route SQL is built as strings, which the typechecker can't see into
$ DATABASE_URL=postgres://localhost/ezfd node scripts/test-queries.cjs

# ezfd-admin.sh backup/restore, round-tripped for SES, FD and empty events
$ PSQL="psql -h localhost -U postgres" bash scripts/test-restore.sh

# The API end to end, against a running server
$ BASE_URL=http://localhost:3000 bash scripts/test-e2e.sh

What each is for

db/test-ses-constraint.sql asserts rather than prints, so a broken constraint fails the build instead of scrolling past. Covers overlap rejection, same-band-different-mode, extension into the next holder, early release, and cancelling a not-yet-started slot.

scripts/test-queries.cjs exercises route SQL through the real pg driver. These queries are assembled as strings, so an interval cast that only works with a text parameter, or a range bound that returns a Date rather than a string, fails at runtime and nowhere earlier.

scripts/test-restore.sh round-trips the admin console's backup and restore. Export and restore are ezfd_export_events() and ezfd_restore_events() in db/schema.sql, so the test exercises the shipped definitions rather than a copy — the same ones the HTTP API and the console call. It used to carry its own third variant of the backup query, and so round-tripped a shape the console's menu action never produced, staying green while that action silently dropped the SES roster.

It also greps ezfd-admin.sh for three things a round trip cannot see, because each of them failed silently:

Guard Catches
No SELECT e.* A hand-rolled export that leaks the encrypted QRZ credentials, as two of the four earlier copies did
No lax PG -v payload= A payload call without ON_ERROR_STOP, which makes a failed restore exit 0 and report success
No IFS='|' read A row reader splitting on a pipe, which a club name containing one silently shifts out of alignment

The middle one is worth a note on how to write this kind of guard. The first version checked that PGS appeared somewhere in the file — and passed even with the restore call reverted, because the count above it also uses PGS and satisfied the grep on its own. A guard over a file with two call sites has to assert the absence of the bad form, not the presence of the good one.

scripts/test-e2e.sh drives the API against a running server: checkout conflicts, both enforcement modes, the offline replay bypass, roster approval, per-operator ADIF, and Field Day regressions.

Writing a test

Check it can fail. Break the thing it guards, watch it go red, put it back. This isn't ceremony — doing it is what revealed that re-applying schema.sql couldn't restore the overlap constraint, because CREATE TABLE IF NOT EXISTS skips the whole statement including its inline constraints.

A test that has never been observed failing is a test you don't know works.

CI

.github/workflows/ci.yml, three jobs:

Job Runs
build Typecheck, build, then the end-to-end suite against the built server
schema Schema applied twice for idempotency, then the constraint, query and restore suites
shell bash -n on every tracked .sh; shellcheck advisory

Lint and shellcheck are deliberately not gated — both report pre-existing findings that would fail every PR. Tracked as an issue; the jobs are wired so enabling them is a one-line change once the backlogs clear.

Conventions

AGENTS.md in the repository root is the authority and is worth reading before changing anything non-obvious. The highlights:

Next.js 16 App Router, standalone output. Route handlers take params: Promise<...> — await them.

Tailwind v4, dark by default. Light-mode styles use the light: prefix, which is the opposite of the usual convention.

Bash scripts use set -uo pipefail without -e. -e terminates interactive menus on the first non-zero return. Use [[ ]] rather than (( )) for comparisons — (( )) returns exit 1 on a false result, which under -e-style handling reads as failure. Always local var="", never bare local var, to avoid unbound-variable errors under set -u.

public/ezfd-rig-bridge.py is a manual copy of the root script, served for direct download. They are not symlinked; copy the root file over the public one after editing.

Where things live

See Architecture for the layout and the shared modules in lib/.

Gotchas that have cost real time

The full list is in AGENTS.md. The ones most likely to catch you:

  • pg returns TIMESTAMPTZ as Date objects, not strings
  • Sections do not multiply the Field Day score
  • Inline arrow-function props retrigger child effects on every parent render, and the logging screen re-renders four times a second under rig control
  • json_agg over zero rows is JSON null, a scalar, which COALESCE misses
  • The SES checkout constraint must stay at band+mode granularity