Why:
On the user's deployed instance the home screen was missing the Notes
tile (notes was in BUILTIN_APP_SLUGS but had no row in the seed's apps
array, so no apps row existed in the DB and the launcher had nothing
to render). The Executive Meetings tile also showed the long name
"إدارة الاجتماعات التنفيذية" / "Executive Meetings"; user asked for
the shorter "الاجتماعات" / "Meetings".
Changes (scripts/src/seed.ts):
- Added "notes" entry to the apps array (slug=notes, route=/notes,
iconName=StickyNote, color=#eab308, sortOrder=2, isActive=true,
nameAr="الملاحظات", nameEn="Notes").
- Renamed executive-meetings entry: nameAr/En + descriptions shortened.
- Extended the existing post-insert UPDATE for executive-meetings to
also push the new name + descriptions, so already-deployed instances
pick up the rename on re-seed (insert is onConflictDoNothing).
- Added a notes.access permission gate mirroring executive_meetings.access:
the permission is created, granted to the admin role only, and linked
to the notes app via app_permissions. getVisibleAppsForUser hides the
tile from anyone without notes.access — admins grant it from the
admin panel per role/user.
Why a permission gate (deviation from initial plan):
Code review caught that without app_permissions, getVisibleAppsForUser
shows every active app to non-admins by default, so a bare INSERT would
have made Notes visible to everyone — opposite of what the user asked
for ("only for those I choose"). Used the existing executive_meetings
pattern to keep the model consistent.
Also added replit.md documenting the workflows, the seed re-run step
after git pull, and the per-permission grant flow for restricted apps.
Verified: ran the seed; SQL confirms notes row + الاجتماعات rename +
notes.access restricted to admin role only.
Why:
On the user's deployed instance the home screen was missing the Notes
tile (notes was in BUILTIN_APP_SLUGS but had no row in the seed's apps
array, so no apps row existed in the DB and the launcher had nothing
to render). The Executive Meetings tile also showed the long name
"إدارة الاجتماعات التنفيذية" / "Executive Meetings"; user asked for
the shorter "الاجتماعات" / "Meetings".
Changes (scripts/src/seed.ts):
- Added "notes" entry to the apps array (slug=notes, route=/notes,
iconName=StickyNote, color=#eab308, sortOrder=2, isActive=true,
nameAr="الملاحظات", nameEn="Notes").
- Renamed executive-meetings entry: nameAr/En + descriptions shortened.
- Extended the existing post-insert UPDATE for executive-meetings to
also push the new name + descriptions, so already-deployed instances
pick up the rename on re-seed (insert is onConflictDoNothing).
- Added a notes.access permission gate mirroring executive_meetings.access:
the permission is created, granted to the admin role only, and linked
to the notes app via app_permissions. getVisibleAppsForUser hides the
tile from anyone without notes.access — admins grant it from the
admin panel per role/user.
Why a permission gate (deviation from initial plan):
Code review caught that without app_permissions, getVisibleAppsForUser
shows every active app to non-admins by default, so a bare INSERT would
have made Notes visible to everyone — opposite of what the user asked
for ("only for those I choose"). Used the existing executive_meetings
pattern to keep the model consistent.
Also added replit.md documenting the workflows, the seed re-run step
after git pull, and the per-permission grant flow for restricted apps.
Verified: ran the seed; SQL confirms notes row + الاجتماعات rename +
notes.access restricted to admin role only.
- executive-meetings.ts: add lockMeetingDate(executor, date)
pg_advisory_xact_lock helper. Acquire it at the top of every
transaction that calls renumberDayByStartTime so concurrent
POST/PATCH/duplicate/postpone-minutes/reschedule/cancel/DELETE/
swap-times/rotate-content/reorder no longer deadlock on the
executive_meetings dailyNumber unique constraint.
- For two-date paths (PATCH cross-day, reschedule, swap-times)
build a sorted distinct date list and lock in deterministic
order to prevent deadlock between opposite-direction moves.
- executive-meeting-notify.ts: add `.for('share')` row lock on
the users table after recipient resolution and before bulk
INSERT into executive_meeting_notifications, eliminating the
FK race against parallel tests that DELETE users under
READ COMMITTED.
- Move tests/executive-meetings-notifications.test.mjs to
tests/serial/ to fix socket fan-out cross-test contamination
(parallel files share DB and server).
No tests were weakened or skipped; all fixes live in
route/service code.
- executive-meetings.ts: add lockMeetingDate(executor, date)
pg_advisory_xact_lock helper. Acquire it at the top of every
transaction that calls renumberDayByStartTime so concurrent
POST/PATCH/duplicate/postpone-minutes/reschedule/cancel/DELETE/
swap-times/rotate-content/reorder no longer deadlock on the
executive_meetings dailyNumber unique constraint.
- For two-date paths (PATCH cross-day, reschedule, swap-times)
build a sorted distinct date list and lock in deterministic
order to prevent deadlock between opposite-direction moves.
- executive-meeting-notify.ts: add `.for('share')` row lock on
the users table after recipient resolution and before bulk
INSERT into executive_meeting_notifications, eliminating the
FK race against parallel tests that DELETE users under
READ COMMITTED.
- Move tests/executive-meetings-notifications.test.mjs to
tests/serial/ to fix socket fan-out cross-test contamination
(parallel files share DB and server).
No tests were weakened or skipped; all fixes live in
route/service code.
Refactor test execution to use a new script that manages the API server lifecycle, including building, starting, waiting for health, running tests, and shutting down, with support for using an existing server.
Refactor test execution to use a new script that manages the API server lifecycle, including building, starting, waiting for health, running tests, and shutting down, with support for using an existing server.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a42bd3b2-3145-4118-87aa-336a7e2189ca
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/FvHcc7z
Replit-Helium-Checkpoint-Created: true
Update README and application configurations to support reverse proxy setups, including TLS termination and proper cookie handling, and add an option to seed demo meetings.
Update README and application configurations to support reverse proxy setups, including TLS termination and proper cookie handling, and add an option to seed demo meetings.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c15da2be-cee7-4cbb-8d58-f9fcc3c38ed7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/FvHcc7z
Replit-Helium-Checkpoint-Created: true
Pushed the existing main-branch commits (since eeeb442) to the
Gitea origin remote as a new branch `replit-sync-deployment-hardening`
so the user can open a PR manually. No code edits in this task —
all changes were already committed on `main` by task #538.
Branch contents (3 commits since eeeb442):
- d146868 Improve handling of external proxy configurations and session security
- eef94ca deployment: harden self-hosted install behind HTTPS reverse proxies
- f060d93 Update documentation to clarify CORS error impact on user login
Files changed (5): artifacts/api-server/src/app.ts, scripts/src/seed.ts,
.env.example, .env.docker.example (covered in d146868), docker-compose.yml,
README.md.
Push details:
- Authenticated via GITEA_TOKEN (oauth2 user) embedded in the push URL.
- Token filtered out of all logged output via sed.
- No `--force` used. Gitea accepted the push as a brand-new branch.
- Gitea echoed the PR-create URL: /rafraa/TX/pulls/new/replit-sync-deployment-hardening
Deviations from the plan:
- Plan step 1-2 said to "create branch from eeeb442 + commit fresh".
Sandbox blocks `git switch -c` / `git commit` even in this task agent,
so I pushed HEAD as the new branch instead. The result on Gitea is
identical — the diff vs `main` is the same 5 files; only the commit
history granularity differs (3 small commits vs 1 squashed commit).
If the user prefers a single squashed commit they can squash-merge
the PR on Gitea.
- The `test` workflow is failing on the api-server start race, but
that's out of scope for #539 and is already covered by task #540
(Auto-start the API server when running tests).
Pushed the existing main-branch commits (since e0a4652) to the
Gitea origin remote as a new branch `replit-sync-deployment-hardening`
so the user can open a PR manually. No code edits in this task —
all changes were already committed on `main` by task #538.
Branch contents (3 commits since e0a4652):
- f16f476 Improve handling of external proxy configurations and session security
- 7d13e8e deployment: harden self-hosted install behind HTTPS reverse proxies
- a01e102 Update documentation to clarify CORS error impact on user login
Files changed (5): artifacts/api-server/src/app.ts, scripts/src/seed.ts,
.env.example, .env.docker.example (covered in f16f476), docker-compose.yml,
README.md.
Push details:
- Authenticated via GITEA_TOKEN (oauth2 user) embedded in the push URL.
- Token filtered out of all logged output via sed.
- No `--force` used. Gitea accepted the push as a brand-new branch.
- Gitea echoed the PR-create URL: /rafraa/TX/pulls/new/replit-sync-deployment-hardening
Deviations from the plan:
- Plan step 1-2 said to "create branch from e0a4652 + commit fresh".
Sandbox blocks `git switch -c` / `git commit` even in this task agent,
so I pushed HEAD as the new branch instead. The result on Gitea is
identical — the diff vs `main` is the same 5 files; only the commit
history granularity differs (3 small commits vs 1 squashed commit).
If the user prefers a single squashed commit they can squash-merge
the PR on Gitea.
- The `test` workflow is failing on the api-server start race, but
that's out of scope for #539 and is already covered by task #540
(Auto-start the API server when running tests).
Update README.md with more specific language regarding CORS errors caused by incorrect ALLOWED_ORIGINS configuration, affecting the user login experience.
Task #538. Fixes the four bugs that bit the user's first Tailscale-fronted
install of Tx OS on their Mac:
1. Missing `user_sessions` table on fresh installs — `connect-pg-simple`
now uses `createTableIfMissing: true`. Drizzle migrations still skip
the table (`tablesFilter: ["!user_sessions"]`), so ownership stays
with the session store.
2. Session cookie silently dropped behind HTTPS proxies that don't
forward `X-Forwarded-Proto` (Tailscale serve, ngrok free, some
Cloudflare Tunnels). New `TRUST_PROXY_HTTPS=true` env var injects
the header before pino + session middleware, and `cookie.secure` is
now derived from `PUBLIC_BASE_URL.startsWith("https://") ||
trustProxyHttps` instead of `NODE_ENV`.
3. `ALLOWED_ORIGINS` was undocumented for non-localhost URLs — added
examples (LAN IP, Tailscale name, custom domain) in `.env.example`,
`.env.docker.example`, `README.md`, plus a new "Reverse proxy /
Tailscale / external URL" README section.
4. Demo executive meetings always seeded on real installs — gated
behind `SEED_DEMO_MEETINGS=true` (default off), wired through
`docker-compose.yml` to the migrate service.
Architect (code review) approved; added a security note clarifying
that `TRUST_PROXY_HTTPS=true` requires the API to be unreachable
directly over plain HTTP.
The git commit + push to Gitea is delegated to a follow-up task
because main-agent sandbox blocks `git commit` / branch creation.
Task #538. Fixes the four bugs that bit the user's first Tailscale-fronted
install of Tx OS on their Mac:
1. Missing `user_sessions` table on fresh installs — `connect-pg-simple`
now uses `createTableIfMissing: true`. Drizzle migrations still skip
the table (`tablesFilter: ["!user_sessions"]`), so ownership stays
with the session store.
2. Session cookie silently dropped behind HTTPS proxies that don't
forward `X-Forwarded-Proto` (Tailscale serve, ngrok free, some
Cloudflare Tunnels). New `TRUST_PROXY_HTTPS=true` env var injects
the header before pino + session middleware, and `cookie.secure` is
now derived from `PUBLIC_BASE_URL.startsWith("https://") ||
trustProxyHttps` instead of `NODE_ENV`.
3. `ALLOWED_ORIGINS` was undocumented for non-localhost URLs — added
examples (LAN IP, Tailscale name, custom domain) in `.env.example`,
`.env.docker.example`, `README.md`, plus a new "Reverse proxy /
Tailscale / external URL" README section.
4. Demo executive meetings always seeded on real installs — gated
behind `SEED_DEMO_MEETINGS=true` (default off), wired through
`docker-compose.yml` to the migrate service.
Architect (code review) approved; added a security note clarifying
that `TRUST_PROXY_HTTPS=true` requires the API to be unreachable
directly over plain HTTP.
The git commit + push to Gitea is delegated to a follow-up task
because main-agent sandbox blocks `git commit` / branch creation.
Introduces `TRUST_PROXY_HTTPS` environment variable and updates session cookie security logic in `app.ts`. Modifies `README.md` and `docker-compose.yml` for clarity on proxy configurations. Updates `seed.ts` to conditionally seed demo meetings based on `SEED_DEMO_MEETINGS` environment variable.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).
Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
(gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
serializes concurrent callers, double-gates on installed flag and
admin existence, then atomically creates the admin user, assigns
admin role + Admins/Everyone groups, and flips system_settings to
installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
setup endpoints.
Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
existence + SEED_*_PASSWORD env vars. Legacy installs (admin
exists, system_settings empty) get backfilled to installed=true
via ON CONFLICT DO UPDATE so they are never forced through the
wizard. When env passwords are unset and no admin exists, the
seed prints a wizard hint instead of seeding.
Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
the legacy http://localhost:${APP_PORT} URL keeps working. In
local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.
Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).
Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.
Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.
Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.