Task: on fresh installs the Calendar and Notifications apps were
appearing enabled by default, and the home screen showed a
Notifications tile that was redundant with the bell icon already in
the top bar. User wanted both apps disabled by default and the home
tile gone (bell stays).
Changes:
- scripts/src/seed.ts: flip the seed `isActive` for `notifications`
and `calendar` from true → false. The seed inserts apps with
`db.insert(appsTable).values(apps).onConflictDoNothing()`, so this
is fully idempotent: existing environments (where the row already
exists with isActive=true) are NOT changed; only fresh inserts on
new installs pick up the disabled default. Admins can enable
either app from app settings when they want it.
- artifacts/tx-os/src/pages/home.tsx: remove "notifications" from
the `dockApps` filter so the bottom dock no longer renders a
Notifications tile. Filter becomes `["services", "admin"]`. The
top-bar bell icon (and its unread badge) was untouched and still
routes to `/notifications`.
Out of scope (per spec): no migration to disable existing prod
rows; no role/permission changes; bell icon in the top bar stays.
Files:
- scripts/src/seed.ts (notifications block ~L229, calendar block ~L275)
- artifacts/tx-os/src/pages/home.tsx:532
Task: on fresh installs the Calendar and Notifications apps were
appearing enabled by default, and the home screen showed a
Notifications tile that was redundant with the bell icon already in
the top bar. User wanted both apps disabled by default and the home
tile gone (bell stays).
Changes:
- scripts/src/seed.ts: flip the seed `isActive` for `notifications`
and `calendar` from true → false. The seed inserts apps with
`db.insert(appsTable).values(apps).onConflictDoNothing()`, so this
is fully idempotent: existing environments (where the row already
exists with isActive=true) are NOT changed; only fresh inserts on
new installs pick up the disabled default. Admins can enable
either app from app settings when they want it.
- artifacts/tx-os/src/pages/home.tsx: remove "notifications" from
the `dockApps` filter so the bottom dock no longer renders a
Notifications tile. Filter becomes `["services", "admin"]`. The
top-bar bell icon (and its unread badge) was untouched and still
routes to `/notifications`.
Out of scope (per spec): no migration to disable existing prod
rows; no role/permission changes; bell icon in the top bar stays.
Files:
- scripts/src/seed.ts (notifications block ~L229, calendar block ~L275)
- artifacts/tx-os/src/pages/home.tsx:532
Task: on iPad the bulk-selected row was nearly invisible (only a 2px
inset navy ring), and the row-tap Quick Actions dialog had only a
Postpone button — user asked for a real visible selection fill and
an Edit (pencil) action wired to the existing edit-meeting flow.
Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- MeetingRow: add `selectionBg = rgba(11,30,63,0.18)` painted on the
row's `<tr>` backgroundColor in addition to the existing
`selectionShadow` 2px navy ring. Composition order in trStyle is
`quickOpenBg ?? selectionBg ?? baseBg` so an open quick-actions
surface still wins (transient action state), user-picked row
colours still show when nothing is selected, and the navy tint
reads clearly on iPad against the table grid.
- MeetingRow: new optional `onQuickEdit?: () => void` prop. Quick
Actions Dialog gains a pencil-icon "Edit" button before the
existing Postpone button, wrapped in `flex flex-wrap gap-2` so the
two buttons sit side-by-side on iPad/desktop and stack on narrow
viewports. Button only renders when a handler is wired
(defensive — `quickActionsCanMutate` already gates the surface).
- ScheduleSection: host a local `editingMeeting` + `editingSaving`
state plus a `MeetingFormDialog` mount, with a save handler whose
body mirrors ManageSection's `save()` exactly (same PATCH payload
projection, same `wrapAsParagraph` / attendee shaping, same
toast/error translation). Edit button on a row opens this in-place
dialog instead of switching the user to the Manage tab.
- Locale: add `executiveMeetings.quickActions.edit` = "تعديل" / "Edit".
Notes:
- No new icon import needed — `Pencil` was already in the lucide
import group.
- Selection ring (`#0B1E3F`) and tint use the same navy so they
read as one cohesive selected state, not competing layers.
- Pre-existing `use-push-subscription.ts` TS noise unchanged.
User asked that when the President (`executive_ceo` role) logs into
the Meetings page he sees a clean "presentation" layout: just the
schedule table, an analog clock plus the weekday/date of the
schedule, and nothing else. The Manage / Notifications / Audit /
PDF tabs and the PDF-export shortcut button are noisy for him and
must disappear — but his backend permissions stay intact.
Changes (frontend only — backend role gates untouched):
- New component `components/executive-meetings/analog-clock.tsx`:
small SVG analog clock (hour/minute/second hands, navy + gold),
ticks every second, ~44px default.
- New inner `PresidentClockBar` component in `executive-meetings.tsx`:
renders the analog clock + weekday + **digital time** (ticking
per second, localized) + selected schedule date in one block.
- `pages/executive-meetings.tsx`:
- Derived `isPresidentView` from the `/me` roles: true iff
`executive_ceo` is present AND the user is NOT also `admin`
or `executive_office_manager` (admins keep full UI).
- Effect forces `section = "schedule"` whenever
`isPresidentView` becomes true, so the president can never
land on a now-hidden tab.
- Hides the header PDF-shortcut button and the entire SECTIONS
tab list when `isPresidentView`. The `#em-toolbar-portal` is
kept so the date picker still mounts.
- `ScheduleSection` accepts two new optional props:
`isPresidentView` and `lang`. When `isPresidentView` is on,
the toolbar renders the AnalogClock next to a label showing
`formatWeekday(date, lang, "long")` and `formatDate(date, lang)`
(e.g. "Sunday · May 17, 2026" / "الأحد · 17 مايو 2026"). The
label updates immediately when the date input changes.
- Edit-mode toggle stays gated on `canMutate`, so the president
(who has canMutate) still gets the inline-edit pencil if he
wants it.
Out of scope: backend role changes, applying the focused view to
other roles, manual presentation-mode toggle. Verified via tsc
(only the pre-existing `use-push-subscription` error remains).
User asked that when the President (`executive_ceo` role) logs into
the Meetings page he sees a clean "presentation" layout: just the
schedule table, an analog clock plus the weekday/date of the
schedule, and nothing else. The Manage / Notifications / Audit /
PDF tabs and the PDF-export shortcut button are noisy for him and
must disappear — but his backend permissions stay intact.
Changes (frontend only — backend role gates untouched):
- New component `components/executive-meetings/analog-clock.tsx`:
small SVG analog clock (hour/minute/second hands, navy + gold),
ticks every second, ~44px default.
- `pages/executive-meetings.tsx`:
- Derived `isPresidentView` from the `/me` roles: true iff
`executive_ceo` is present AND the user is NOT also `admin`
or `executive_office_manager` (admins keep full UI).
- Effect forces `section = "schedule"` whenever
`isPresidentView` becomes true, so the president can never
land on a now-hidden tab.
- Hides the header PDF-shortcut button and the entire SECTIONS
tab list when `isPresidentView`. The `#em-toolbar-portal` is
kept so the date picker still mounts.
- `ScheduleSection` accepts two new optional props:
`isPresidentView` and `lang`. When `isPresidentView` is on,
the toolbar renders the AnalogClock next to a label showing
`formatWeekday(date, lang, "long")` and `formatDate(date, lang)`
(e.g. "Sunday · May 17, 2026" / "الأحد · 17 مايو 2026"). The
label updates immediately when the date input changes.
- Edit-mode toggle stays gated on `canMutate`, so the president
(who has canMutate) still gets the inline-edit pencil if he
wants it.
Out of scope: backend role changes, applying the focused view to
other roles, manual presentation-mode toggle. Verified via tsc
(only the pre-existing `use-push-subscription` error remains).
User requested that deleting a finished order from My Orders happen
silently — one click on the trash icon, no confirmation dialog, no
success toast. Just click and the card disappears.
Changes:
- Removed the delete-confirm AlertDialog from OrderCard along with
its `confirmDeleteOpen` state. The trash icon now calls
`handleDelete` directly.
- Removed `toast({ title: t("myOrders.deleted") })` from
`scheduleDelete`. The 7-second soft-delete grace window stays in
place (pendingDeleteIds filter + setTimeout) so the API DELETE
still fires after the window. The `deleteFailed` error toast is
preserved so silent API failures don't lose orders.
- Dropped the now-unused locale keys from ar.json and en.json:
`deleteConfirmTitle`, `deleteConfirmBody`, `deleteConfirm`,
`deleted`.
- Removed the unused `AlertDialogDescription` import (the remaining
cancel-confirm dialog uses only the title).
Cancel-order flow remains untouched (still shows confirm dialog and
undo toast as in #566).
Per user request on the طلباتي page:
1. Removed the "Clear finished orders" bulk button entirely. The button
was showing as a raw i18n key (myOrder c...) for the user and they
asked for it gone. Dropped the related AlertDialog, confirmClearOpen
state, handleClearFinished handler, and the locale keys
clearFinished_*, clearConfirmTitle/Body_*, clearConfirm,
clearedCount_*, and clearedPartial in both ar.json and en.json.
2. Removed the "تراجع / Undo" ToastAction shown after deleting an
order. The toast now displays only the "Order deleted" title; the
7-second timer + pendingDeleteIds soft-delete window is preserved
so the actual DELETE still fires after the grace period. The undo
closure was deleted since nothing references it anymore. Single-id
call site stays untouched (scheduleDelete still receives [id]).
3. Removed the "ستتاح لك ثوانٍ قليلة للتراجع" description from the
cancel-order confirmation dialog. The cancelConfirmBody key was
dropped from both locales. The AlertDialog now shows only the
title + the two action buttons.
Out of scope (kept as-is):
- The undo ToastAction after CANCEL (user only asked about delete).
- Incoming-orders page.
Notes:
- ToastAction import is still used by the cancel-undo path.
- Trash2 import still used by the per-card delete button.
- Pre-existing TS error in use-push-subscription.ts is unrelated.
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).
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).
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.
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
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
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.
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.
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.
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.
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.
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).
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.
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).
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.
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.
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.
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