Commit Graph

948 Commits

Author SHA1 Message Date
riyadhafraa 620ab284e2 fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on
iPad/mobile. Six fixes, all client-side:

1. Quick-actions surface now opens from ANY click on a meeting row
   (was: only the attendees cell). Root cause: handleRowClick guard
   skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*`
   descendants. Those affordances only do anything in edit mode, but
   handleRowClick already early-returns when edit mode is on, so the
   testid guard was always over-aggressive. Dropped those guards plus
   the `[role='button']` rule from the interactive selector, kept the
   real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc.
   guards and the `em-row-actions-*` / `em-row-select-*` testid
   guards (those affordances are visible outside edit mode and must
   not trigger the surface).
   File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350).

2. Originating row is now unmistakable on iPad: quickOpenShadow is
   `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single
   3px inset) and the row background tints to
   `rgba(highlight, 0.08)` while quick-actions is open.
   File: artifacts/tx-os/src/pages/executive-meetings.tsx
   (~L4287, L4344).

3. Postpone dialog header was overlapping the Radix close X in RTL.
   Added `pr-8` on the DialogHeader (same pattern already used for
   the row-quick mini-dialog).
   File: artifacts/tx-os/src/components/executive-meetings/
   upcoming-meeting-alert.tsx (~L1447).

4. Reschedule tab: date / start / end inputs collided on iPad
   portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column
   through iPad portrait, three across on landscape/desktop), with
   `min-w-0` on each cell and `w-full` on every Input so Safari's
   native date/time pickers stop overflowing.
   Same file (~L1761).

5. Cancel tab X overlap is resolved by the same DialogHeader fix in
   step 3 (one Dialog wraps all three tabs).

6. Removed the `cancelHint` paragraph + the `cancelHint` translation
   keys in ar.json (L1252) and en.json (L1164) — fully unused now.

NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky).
Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this).
2026-05-17 10:18:13 +00:00
riyadhafraa 5834036707 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d8f29dc1-570e-487c-8034-8fc616ca80dd
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fNoqq4C
Replit-Helium-Checkpoint-Created: true
2026-05-17 10:16:03 +00:00
riyadhafraa 0f71ae4102 feat(executive-meetings): grant executive_ceo mutate + audit + push notify (Task #561)
User request: السماح للرئيس (executive_ceo) بإنشاء/تعديل/تأجيل/حذف
الاجتماعات، استلام إشعارات Push للتذكير، والاطلاع على سجل التدقيق —
مع إبقاء إعدادات الخطوط/الشعار حصرية على مدير المكتب التنفيذي.

Changes (3 lines added, backend-only):

1. artifacts/api-server/src/routes/executive-meetings.ts
   - Added "executive_ceo" to MUTATE_ROLES (line 120) → POST/PATCH/DELETE
     on meetings now accepted for the CEO role.
   - Added "executive_ceo" to ADMIN_AUDIT_ROLES (line 130) → /audit
     endpoint now reachable; canViewAudit=true in the bootstrap payload.
   - EM_ADMIN_ROLES intentionally untouched, so font-settings and global
     PDF brand assets remain admin + executive_office_manager only.

2. artifacts/api-server/src/lib/executive-meeting-scheduler.ts
   - Added "executive_ceo" to EM_NOTIFY_ROLES (line 18) → CEO users with
     an active Web Push subscription now receive the LEAD_MINUTES=5
     pre-meeting reminder push.

Frontend requires no changes: it gates buttons on canMutate /
canViewAudit booleans returned by the bootstrap endpoint (lines
617-619), which flip to true automatically for the CEO role.

Verification: API workflow restarted clean, scheduler started OK,
/api/executive-meetings and /alert-state respond 304/200.

NOT pushed to Gitea: Tailscale link to desktop-11cj93j is still down
from this sandbox (HTTP=000). Task #560's earlier VAPID fix
(629069b1) is also still pending push. Both will go up together when
the tunnel recovers, or the user can apply the same one-line edits
on the Mac and rebuild the api container.
2026-05-17 10:07:57 +00:00
riyadhafraa 42b881355b Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0cdfc11a-cdea-4382-82a8-d1997a8d0e3d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fNoqq4C
Replit-Helium-Checkpoint-Created: true
2026-05-17 10:06:38 +00:00
riyadhafraa 629069b1fe Fix VAPID subject fallback for push notifications
Update push notification logic to correctly handle missing VAPID_SUBJECT environment variables by using logical OR operator instead of nullish coalescing for subject fallback.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 96637a28-4f37-4966-8adb-95f8f66ee769
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fNoqq4C
Replit-Helium-Checkpoint-Created: true
2026-05-17 09:55:57 +00:00
riyadhafraa 9fa40f00ab Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b520895c-b60d-4b2b-a96d-62499515c77d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX
Replit-Helium-Checkpoint-Created: true
2026-05-17 06:58:43 +00:00
riyadhafraa c1e3a81cae Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 679db8c7-6a6d-410a-b781-5112c524b87d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX
Replit-Helium-Checkpoint-Created: true
2026-05-17 06:50:54 +00:00
riyadhafraa 51c3d8a8ba Fix toggle switch behavior in right-to-left reading modes
Update the switch component's CSS to correctly position the toggle thumb in RTL layouts for both checked and unchecked states.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ee6229a6-5472-4ca0-8f84-74b229703438
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX
Replit-Helium-Checkpoint-Created: true
2026-05-16 17:46:43 +00:00
riyadhafraa f4253d4d9d Improve toggle switch behavior in right-to-left languages
Add custom RTL and LTR variants to Tailwind CSS configuration to correctly position toggle switch indicators in Arabic and other right-to-left languages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 84451adc-9c86-40d5-80ba-846e7ea2a813
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX
Replit-Helium-Checkpoint-Created: true
2026-05-16 17:21:39 +00:00
riyadhafraa 05446afb5d Improve meeting reminders with new scheduler and improved push notifications
Add a new background scheduler to trigger meeting reminders, implement atomic claims for push notifications to prevent duplicates, and add an `ignoreConnected` option to push notifications.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a63187bd-4947-4e1f-8959-e8d68ff96a8c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX
Replit-Helium-Checkpoint-Created: true
2026-05-16 17:13:19 +00:00
riyadhafraa 55d19298e7 Add bilingual support for notifications and automatic HTTPS
Implement bilingual text for push notifications, allowing users to receive them in their preferred language. Automatically configure HTTPS with Let's Encrypt for custom domains to ensure persistent subscriptions.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 94f06aee-a5cf-45df-979e-f940cce77214
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/yEFtMKN
Replit-Helium-Checkpoint-Created: true
2026-05-16 16:28:42 +00:00
riyadhafraa d78818b88a feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe`, and a spec-aligned alias
  `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`, gated on
  `serviceWorker` AND `PushManager` support so older browsers no-op
  cleanly.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3
  passing — true in-process unit test (`node --import tsx
  --experimental-test-module-mocks`) that mocks `web-push` to verify
  the prune path: 410 deletes the row, 404 deletes the row, and a
  transient 500 leaves the row intact. `tsx` added as a devDep and the
  test:run script picks up `*.test.ts` alongside the existing `.mjs`
  suites.
- OpenAPI updated: `DELETE /push/subscribe` documented with an
  `endpoint` query parameter alongside the existing POST routes; orval
  codegen re-run so the generated client + zod schemas stay in sync.
- Restored the unrelated serial tests
  (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`)
  that an earlier cleanup pass dropped — they are unchanged from prior
  green state.
- Architect rounds addressed:
  1. race + payload size.
  2. dedup gate + first-launch UX + SW base-path + initial tests.
  3. DELETE alias + PushManager check + restored serial tests + real
     410/404 cleanup test + OpenAPI parity.
  4. .env.docker.example + replit.md operational docs for VAPID,
     DELETE-alias integration tests (auth + happy-path + bad input),
     and an explicit design-note comment in `push.ts` documenting the
     intentional user-wide dedup policy.
2026-05-16 15:44:08 +00:00
riyadhafraa b5720c9aac feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe`, and a spec-aligned alias
  `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`, gated on
  `serviceWorker` AND `PushManager` support so older browsers no-op
  cleanly.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3
  passing — true in-process unit test (`node --import tsx
  --experimental-test-module-mocks`) that mocks `web-push` to verify
  the prune path: 410 deletes the row, 404 deletes the row, and a
  transient 500 leaves the row intact. `tsx` added as a devDep and the
  test:run script picks up `*.test.ts` alongside the existing `.mjs`
  suites.
- OpenAPI updated: `DELETE /push/subscribe` documented with an
  `endpoint` query parameter alongside the existing POST routes; orval
  codegen re-run so the generated client + zod schemas stay in sync.
- Restored the unrelated serial tests
  (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`)
  that an earlier cleanup pass dropped — they are unchanged from prior
  green state.
- Architect rounds addressed:
  1. race + payload size.
  2. dedup gate + first-launch UX + SW base-path + initial tests.
  3. DELETE alias + PushManager check + restored serial tests + real
     410/404 cleanup test + OpenAPI parity.
2026-05-16 15:42:29 +00:00
riyadhafraa 54ca20a00f feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe`, and a spec-aligned alias
  `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`, gated on
  `serviceWorker` AND `PushManager` support so older browsers no-op
  cleanly.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3
  passing — true in-process unit test (`node --import tsx
  --experimental-test-module-mocks`) that mocks `web-push` to verify
  the prune path: 410 deletes the row, 404 deletes the row, and a
  transient 500 leaves the row intact. `tsx` added as a devDep and the
  test:run script picks up `*.test.ts` alongside the existing `.mjs`
  suites.
- OpenAPI updated: `DELETE /push/subscribe` documented with an
  `endpoint` query parameter alongside the existing POST routes; orval
  codegen re-run so the generated client + zod schemas stay in sync.
- Restored the unrelated serial tests
  (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`)
  that an earlier cleanup pass dropped — they are unchanged from prior
  green state.
- Architect rounds addressed:
  1. race + payload size.
  2. dedup gate + first-launch UX + SW base-path + initial tests.
  3. DELETE alias + PushManager check + restored serial tests + real
     410/404 cleanup test + OpenAPI parity.
2026-05-16 15:41:21 +00:00
riyadhafraa 0740971a96 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe`, and a spec-aligned alias
  `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`, gated on
  `serviceWorker` AND `PushManager` support so older browsers no-op
  cleanly.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3
  passing — true in-process unit test (`node --import tsx
  --experimental-test-module-mocks`) that mocks `web-push` to verify
  the prune path: 410 deletes the row, 404 deletes the row, and a
  transient 500 leaves the row intact. `tsx` added as a devDep and the
  test:run script picks up `*.test.ts` alongside the existing `.mjs`
  suites.
- OpenAPI updated: `DELETE /push/subscribe` documented with an
  `endpoint` query parameter alongside the existing POST routes; orval
  codegen re-run so the generated client + zod schemas stay in sync.
- Restored the unrelated serial tests
  (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`)
  that an earlier cleanup pass dropped — they are unchanged from prior
  green state.
- Architect rounds addressed:
  1. race + payload size.
  2. dedup gate + first-launch UX + SW base-path + initial tests.
  3. DELETE alias + PushManager check + restored serial tests + real
     410/404 cleanup test + OpenAPI parity.
2026-05-16 15:39:41 +00:00
riyadhafraa bf39a6eda6 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe`, and a spec-aligned alias
  `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`, gated on
  `serviceWorker` AND `PushManager` support so older browsers no-op
  cleanly.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- Architect rounds addressed in order:
  1. race + payload size — fixed.
  2. dedup gate + first-launch UX + SW base-path + tests — fixed.
  3. DELETE alias + PushManager check — fixed in this commit. A true
     410-cleanup test was attempted but reaching the prune branch from
     an out-of-process .mjs test requires mocking the `web-push`
     module, which the current `node --test` + bundled-dist harness
     doesn't support cleanly; the 404/410 catch branch is small,
     self-contained, and was code-reviewed. Tracked as a follow-up.
2026-05-16 15:35:42 +00:00
riyadhafraa 8f3b750961 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- Two architect rounds: first flagged race + payload size (fixed),
  second flagged dedup gate + first-launch UX + SW base-path + tests
  (all fixed in this commit).
2026-05-16 15:29:28 +00:00
riyadhafraa 243ccb3e00 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker
    volume) with /tmp fallback for Replit dev, else ephemeral in-memory.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes so over-sized notes don't kill delivery.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (account switch on shared
    device) so the previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into the 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only
  (no asset caching). Focuses an existing tab or opens a new one at the
  payload URL.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status:
  unsupported / denied / default / subscribed).
- New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings
  (notifSettings.push.*). iPad copy explains that the user must add to
  Home Screen first for iOS to allow Web Push.

Plumbing
- OpenAPI: 3 new operations under `notifications` tag; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the generated
  key; subscribe endpoint 401s without auth as expected.
- Architect review surfaced two HIGH issues (account-switch leak,
  payload size); both fixed before completion.
2026-05-16 15:26:18 +00:00
riyadhafraa d91a68bc68 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c32c85bc-ba42-427b-945b-c189acf14d71
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-16 15:18:48 +00:00
riyadhafraa e9d7b8dc76 Fix service images + Arabic ي dots on home tiles (Task #552)
Two visible bugs reported from the iPad after Task #551 deploy:

1) Only "قهوة سعودي" showed its image on /services. Three other
   seeded services (شاي، بلاك كوفي، عصير طازج) showed a broken-image
   placeholder because the seed only set `imageUrl` for Saudi Coffee.

2) The dots under final ي in the home-grid tile label "خدماتي"
   didn't render on iOS Safari. The label `<span>` didn't pin a
   font-family, so iOS fell back to a Latin face that lacks proper
   Arabic glyphs.

Changes:

- scripts/src/seed.ts:
  • Added `imageUrl` for tea/black-coffee/juice in the insert array
    (covers fresh installs).
  • Added a backfill loop AFTER the insert that fills `imageUrl` on
    existing rows only when the value is currently NULL. Admin-
    customized image URLs are never overwritten — matches the
    existing legacy-rename pattern at lines 395-423.

- artifacts/tx-os/src/pages/home.tsx (AppIconContent label):
  • Added `lang` and `dir` attributes on the tile `<span>`.
  • Pinned an explicit font stack: DIN Next LT Arabic → Helvetica
    Neue LT Arabic → Tajawal for Arabic; Helvetica Neue → system-ui
    for English. The three Arabic faces are already declared in
    custom-fonts.css.

No schema changes, no new dependencies. Verified typecheck passes
and HMR reloaded both files cleanly in the dev server.

Push to Gitea is a separate follow-up (git commit is restricted in
the main agent; platform commits this task's changes on completion,
and the push task can then mirror them to the Mac).
2026-05-16 10:18:07 +00:00
riyadhafraa 51a401b6e5 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e49c87fa-b03f-4bb0-8b1e-7d0c9ef599fb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-16 10:16:24 +00:00
riyadhafraa 66f063a978 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 4285357b-389f-4cc3-b20e-906440f5c155
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-16 08:54:18 +00:00
riyadhafraa a6d5988421 Add PWA manifest + icons so iPad opens Tx OS full-screen (Task #550)
Problem: on the iPad the Safari URL bar stayed visible even after
"Add to Home Screen" because index.html had no PWA manifest and no
Apple PWA meta tags — the home-screen icon opened a normal Safari tab.

Changes:
- New `artifacts/tx-os/public/manifest.webmanifest`:
  name/short_name "Tx OS", start_url "/", scope "/",
  display "standalone", background_color and theme_color "#0B1E3F"
  (matches the Meetings brand), and 3 icons (192/512 "any" + 512
  "maskable" with safe-area padding).
- Generated icon PNGs from the existing `public/favicon.svg` brand
  mark via ImageMagick:
    public/icons/icon-192.png            (192x192)
    public/icons/icon-512.png            (512x512)
    public/icons/icon-maskable-512.png   (512x512, 360x360 logo
                                          centered on #0B1E3F so iOS/
                                          Android masks don't crop)
    public/apple-touch-icon.png          (180x180, iPad/iPhone home)
- `artifacts/tx-os/index.html` head: added <link rel="manifest">,
  <link rel="apple-touch-icon">, plus apple-mobile-web-app-capable,
  mobile-web-app-capable, apple-mobile-web-app-status-bar-style
  (default), apple-mobile-web-app-title, and theme-color meta tags.
- `replit.md`: new "iPad / iPhone — open Tx OS full-screen" section
  with the one-time Add-to-Home-Screen steps and the gotcha that
  pre-existing shortcuts must be removed and re-added to pick up the
  manifest.

Verified: dev server returns 200 for /manifest.webmanifest,
/apple-touch-icon.png, and /icons/icon-192.png. The new files all
live under public/ so they're copied verbatim into the nginx image
on the next docker build.
2026-05-16 08:51:25 +00:00
riyadhafraa b69bcd4241 Improve deployment script reliability and add explicit seed step
Update redeploy.sh to include an explicit seed step and adjust script numbering and help output.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ca6e30a4-b773-4707-8e0c-a72cb5bddf70
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-16 08:50:33 +00:00
riyadhafraa 3c8bfbf84d Document Docker rebuild after git pull (Task #549)
Root cause of "service images don't show on the Mac after pulling latest":
the SPA is baked into the nginx (`web`) image and the API binary is baked
into the `api` image at `docker compose build` time. Running just
`git pull && docker compose up -d` reuses the existing images, so new
static assets in `artifacts/tx-os/public/` (and any front-end / API code
changes) never reach the browser.

Changes:
- New `scripts/redeploy.sh` — single-command redeploy. Steps:
  1. git pull --ff-only (skip with --no-pull)
  2. docker compose build api web
  3. docker compose run --rm migrate  (drizzle push + idempotent seed)
  4. docker compose up -d
  Detects `docker compose` v2 vs legacy `docker-compose`, runs from any
  cwd via BASH_SOURCE, set -euo pipefail, --help prints the header.
  chmod +x. Verified `bash -n` and `--help`.
- `replit.md` — new "Redeploying after `git pull` (Docker hosts)"
  section that calls out the gotcha and points at the script.
- `DEPLOYMENT_MIGRATION.md` — one-line cross-reference at the top of
  the deploy section so anyone reading deploy docs finds the script.

No code or runtime behavior changed. No follow-ups proposed: the only
natural next step (PWA full-screen) is already tracked as Task #550.
2026-05-16 08:49:12 +00:00
riyadhafraa 46ac734a8e Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7b9229d0-2fbc-4a67-a10e-a4461d2c4823
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-16 08:48:02 +00:00
riyadhafraa 92425aafe6 Update internal notes order to display correctly
Adjust the sort order for internal notes to ensure proper display sequence.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 418871f8-1a80-43b8-948e-252be1122ed8
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-16 08:36:26 +00:00
riyadhafraa 097276f8cc Task #545: Add Notes app to seed + rename Executive Meetings → Meetings
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.
2026-05-16 08:35:15 +00:00
riyadhafraa 9eab759acb Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 20a01eb7-fdd9-426a-90e4-3dbbc503b2e5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-16 08:30:38 +00:00
rafraa c69e6903c1 Delete .canvas/assets/asset_2133256110.png 2026-05-16 08:18:47 +00:00
rafraa 86b264f20a Delete .canvas/assets/asset_556433877.png 2026-05-16 08:18:47 +00:00
Riydx0 2679096e63 chore: add dev workflows for tx-os + api-server 2026-05-15 13:55:50 +00:00
riyadhafraa b10ee5392c Saved progress at the end of the loop
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1194403f-51ef-43ba-9710-3d091b4840f6
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/T9daF3x
Replit-Helium-Checkpoint-Created: true
2026-05-15 12:43:53 +00:00
riyadhafraa 66e0e5697f Task #542: fix api-server test deadlocks/races (all 327+13 tests green)
- 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.
2026-05-15 11:41:22 +00:00
riyadhafraa 2889148a0e Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f5807abb-6742-4351-91e4-87cc3f7286c8
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
2026-05-15 10:53:24 +00:00
riyadhafraa a5a35ef217 Improve test execution by automatically starting and stopping the API 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
2026-05-15 10:48:48 +00:00
riyadhafraa 77f8096deb Improve security and deployment flexibility for external access
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
2026-05-15 10:38:44 +00:00
riyadhafraa fa31fb6374 task #539: push deployment-hardening branch to Gitea
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).
2026-05-15 10:36:43 +00:00
riyadhafraa a01e102a08 Update documentation to clarify CORS error impact on user login
Update README.md with more specific language regarding CORS errors caused by incorrect ALLOWED_ORIGINS configuration, affecting the user login experience.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 201c4794-de42-4660-9fb4-83343d5bda24
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
2026-05-15 10:35:01 +00:00
riyadhafraa 7d13e8e345 deployment: harden self-hosted install behind HTTPS reverse proxies
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.
2026-05-15 10:33:29 +00:00
riyadhafraa f16f4763df Improve handling of external proxy configurations and session security
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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 097c59e1-8bbe-4846-a79e-3e661e6645c4
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
2026-05-15 10:27:35 +00:00
riyadhafraa e0a4652947 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6a631a61-6adb-4703-8cb4-9afd32d0a7ad
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
2026-05-15 10:19:42 +00:00
riyadhafraa e71ab51541 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 75c6d3ef-cc1b-4fb0-ba39-fff6815eb92f
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
2026-05-14 10:37:43 +00:00
riyadhafraa 154c223d8c Update documentation to include setup scripts and remove server type references
Refactors the README.md file to detail installation scripts and replace "VPS" with more general server terminology.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a4f33306-93f4-413c-b24b-58e08633a127
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
2026-05-14 10:16:55 +00:00
riyadhafraa 87a62f8089 feat(setup): Stage 1 first-time setup wizard backend (no UI)
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.
2026-05-14 08:15:49 +00:00
riyadhafraa ca1508b04f feat(setup): Stage 1 first-time setup wizard backend (no UI)
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.
2026-05-14 08:13:57 +00:00
riyadhafraa dba320fe35 feat(setup): Stage 1 first-time setup wizard backend (no UI)
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.
2026-05-14 08:11:43 +00:00
riyadhafraa 1b2567f3e4 feat(setup): Stage 1 first-time setup wizard backend (no UI)
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.
2026-05-14 08:09:53 +00:00
riyadhafraa f6ff70cd2d feat(setup): Stage 1 first-time setup wizard backend (no UI)
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.
2026-05-14 08:08:02 +00:00
riyadhafraa 340017beaa feat(setup): Stage 1 first-time setup wizard backend (no UI)
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.
2026-05-14 08:04:48 +00:00