Commit Graph

465 Commits

Author SHA1 Message Date
riyadhafraa c40384b0d9 #570: stronger selected-row highlight + Edit button in Quick Actions
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.
2026-05-17 14:40:34 +00:00
riyadhafraa dc30d75e11 Hide redundant clock and navigation elements for president view
Modify executive-meetings.tsx to conditionally hide the HeaderClock and section navigation when isPresidentView is true.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e1c6509c-638c-4c43-8d4f-4b4a5910fc4a
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 14:34:59 +00:00
riyadhafraa 21dda3d372 #572 president-only focused view for executive meetings
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).
2026-05-17 14:34:12 +00:00
riyadhafraa 27384d019a #572 president-only focused view for executive meetings
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).
2026-05-17 14:32:19 +00:00
riyadhafraa 4b151c50a8 #568 silent delete for finished orders
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).
2026-05-17 14:13:42 +00:00
riyadhafraa 4c57c58f8c #566 clean up My Orders page (3 removals)
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.
2026-05-17 14:07:23 +00:00
riyadhafraa 4e5bed602e 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:19:16 +00:00
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 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 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 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 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 40b5f7773c Update website's default sharing image
Update opengraph.jpg in the public artifacts directory.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6a7af115-92ba-48dc-a8b6-068ee87c1c7d
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 07:00:21 +00:00
riyadhafraa 7a2ae8434d Update project documentation and code comments to remove platform-specific references
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu
Replit-Helium-Checkpoint-Created: true
2026-05-14 06:23:49 +00:00
riyadhafraa 3af737186d Improve admin modal design and accessibility with shared component
Refactors AdminFormDialog to create a reusable component for admin modals, including aria-labelledby/describedby attributes and Escape key closing functionality.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fa532550-4873-409b-840a-ac4c474132f3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu
Replit-Helium-Checkpoint-Created: true
2026-05-13 19:11:10 +00:00
riyadhafraa a15365f459 Task #531: Redesign DeletionWarningDialog for a more professional look
Scope: artifacts/tx-os/src/pages/admin.tsx — the shared confirm
dialog used for deleting Apps, Services, and Users on the admin
page.

What changed
- Three explicit visual regions instead of a flat stack:
  - Header: AlertTriangle icon in a destructive-tinted circle next
    to the bold title (which already contains the item name from
    the existing translation strings).
  - Body: warning copy, then impacted items rendered inside a
    sub-card (rounded border + subtle bg + custom bullet dots
    instead of `list-disc`), then a destructive-tinted callout
    holding the "cannot be undone" hint.
  - Footer: separated by a top border and a faint bg tint;
    Cancel = ghost (quieter), Confirm/Anyway = destructive (clear
    primary action). When `isPending`, a small spinner appears
    inside the destructive button.
- Slightly larger max-width (max-w-md), shadow-xl, and overflow-
  hidden so the rounded corners read cleanly with the new footer
  divider.
- `role="alertdialog"` + `aria-modal="true"` added on the panel.

Behaviour preserved
- Component props, call sites, and all `data-testid` hooks are
  unchanged (testId, confirmTestId — covers app-delete-dialog/
  -confirm, service-delete-dialog/-confirm, user-delete-dialog/
  -confirm).
- No translation keys added; reuses every existing
  admin.deleteApp/Service/User.* string.
- Cancel renders before Confirm in the DOM so tab order is
  Cancel -> Destructive (no a11y regression). Visual order is
  controlled with `justify-end`, which mirrors correctly in RTL.
- Pure UI change: no API/handler/permission logic touched.

Notes
- Architect flagged an earlier `flex-row-reverse` footer for
  putting the destructive button first in tab order; fixed before
  marking complete.
- Pre-existing TS18046 / TS2345 errors in admin.tsx are unrelated.
2026-05-13 15:50:54 +00:00
riyadhafraa 21ccb6c426 Task #531: Redesign DeletionWarningDialog for a more professional look
Scope: artifacts/tx-os/src/pages/admin.tsx — the shared confirm
dialog used for deleting Apps, Services, and Users on the admin
page.

What changed
- Three explicit visual regions instead of a flat stack:
  - Header: AlertTriangle icon in a destructive-tinted circle next
    to the bold title (which already contains the item name from
    the existing translation strings).
  - Body: warning copy, then impacted items rendered inside a
    sub-card (rounded border + subtle bg + custom bullet dots
    instead of `list-disc`), then a destructive-tinted callout
    holding the "cannot be undone" hint.
  - Footer: separated by a top border and a faint bg tint;
    Cancel = ghost (quieter), Confirm/Anyway = destructive (clear
    primary action). When `isPending`, a small spinner appears
    inside the destructive button.
- Slightly larger max-width (max-w-md), shadow-xl, and overflow-
  hidden so the rounded corners read cleanly with the new footer
  divider.
- `role="alertdialog"` + `aria-modal="true"` added on the panel.

Behaviour preserved
- Component props, call sites, and all `data-testid` hooks are
  unchanged (testId, confirmTestId — covers app-delete-dialog/
  -confirm, service-delete-dialog/-confirm, user-delete-dialog/
  -confirm).
- No translation keys added; reuses every existing
  admin.deleteApp/Service/User.* string.
- Cancel renders before Confirm in the DOM so tab order is
  Cancel -> Destructive (no a11y regression). Visual order is
  controlled with `justify-end`, which mirrors correctly in RTL.
- Pure UI change: no API/handler/permission logic touched.

Notes
- Architect flagged an earlier `flex-row-reverse` footer for
  putting the destructive button first in tab order; fixed before
  marking complete.
- Pre-existing TS18046 / TS2345 errors in admin.tsx are unrelated.
2026-05-13 15:49:47 +00:00
riyadhafraa 60fbd3dd84 Task #529: Restore topbar logout + collapsible Settings groups
home.tsx
- Re-import LogOut from lucide-react.
- Append a single-tap Logout icon button after <SettingsPanel /> in
  the right-side cluster, wired to the existing handleLogout flow.
- Drop the onLogout prop from <SettingsPanel />; update the topbar
  comment block to describe the Bell -> Inbox -> Settings -> Logout
  cluster.

settings-panel.tsx
- Drop the LogOut import, the onLogout prop on SettingsPanel and
  SettingsPanelBody, the local handleLogout wrapper, and the inline
  logout button (data-testid="settings-logout").
- Wrap the four groups (language, notifications, sound, clock) in
  the existing shadcn Accordion (type="multiple") so each toggles
  independently. Default = all collapsed; open state is lifted to
  SettingsPanel via useState<GroupId[]> so toggles persist while the
  panel is closed and re-opened in the same session.
- New GroupItem helper styles each AccordionItem like the prior
  GroupCard so the panel keeps its rounded-card visual rhythm. The
  trigger uses text-start so headers align correctly in RTL.
- Existing form bodies (LanguageBody, NotificationSettingsContent,
  SoundBody, ClockStyleContent) are reused unchanged, preserving all
  DB + localStorage persistence.

Notes
- Logout placement and removal from the panel were both explicit user
  requests (clarified in plan #529 after #527 unified the topbar).
- No new i18n keys; existing settingsPanel.section.* labels reused
  as accordion triggers.
- typecheck clean (pre-existing TS6305/TS7006 noise unrelated).
2026-05-13 15:15:31 +00:00
riyadhafraa 7abb0c870b Task #527: unify topbar into per-user Settings panel
Topbar right cluster collapses from 5–7 icons to exactly Bell -> Inbox
(conditional on canReceiveOrders) -> Settings (gear). Logout moved
inside the Settings panel; the second bell, globe, volume and clock
buttons are gone from the topbar.

New SettingsPanel renders as a Popover on md+ and a bottom Sheet on
<md (via new useMediaQuery hook). It composes existing controls
rather than re-implementing them:
- Language radio uses useUpdateLanguage + i18n.changeLanguage
- Notifications group renders extracted NotificationSettingsContent
- Sound group toggles notificationsMuted (same flag QuickMute used)
- Clock group renders extracted ClockStyleContent
All DB + localStorage persistence is preserved unchanged.

Refactors:
- notification-settings.tsx: extracted NotificationSettingsContent;
  NotificationSettingsPicker now wraps it (kept for any future caller).
- clock-style-picker.tsx: extracted ClockStyleContent; ClockStylePicker
  now wraps it; removed dead setOpen reference inside choose().
- home.tsx: removed Globe / QuickMute / NotificationSettingsPicker /
  ClockStylePicker / standalone LogOut from the cluster, dropped
  related imports + useUpdateLanguage, reordered to Bell -> Inbox ->
  Settings, and passes onLogout to SettingsPanel.

i18n: added settingsPanel.{title, section.*, lang.*, sound.master}
keys to ar.json and en.json.

Code review: PASS.
2026-05-13 14:55:53 +00:00
riyadhafraa e6a3b148f4 Task #527: unify topbar into per-user Settings panel
Topbar right cluster collapses from 5–7 icons to exactly Bell -> Inbox
(conditional on canReceiveOrders) -> Settings (gear). Logout moved
inside the Settings panel; the second bell, globe, volume and clock
buttons are gone from the topbar.

New SettingsPanel renders as a Popover on md+ and a bottom Sheet on
<md (via new useMediaQuery hook). It composes existing controls
rather than re-implementing them:
- Language radio uses useUpdateLanguage + i18n.changeLanguage
- Notifications group renders extracted NotificationSettingsContent
- Sound group toggles notificationsMuted (same flag QuickMute used)
- Clock group renders extracted ClockStyleContent
All DB + localStorage persistence is preserved unchanged.

Refactors:
- notification-settings.tsx: extracted NotificationSettingsContent;
  NotificationSettingsPicker now wraps it (kept for any future caller).
- clock-style-picker.tsx: extracted ClockStyleContent; ClockStylePicker
  now wraps it; removed dead setOpen reference inside choose().
- home.tsx: removed Globe / QuickMute / NotificationSettingsPicker /
  ClockStylePicker / standalone LogOut from the cluster, dropped
  related imports + useUpdateLanguage, reordered to Bell -> Inbox ->
  Settings, and passes onLogout to SettingsPanel.

i18n: added settingsPanel.{title, section.*, lang.*, sound.master}
keys to ar.json and en.json.

Code review: PASS.
2026-05-13 14:53:53 +00:00
riyadhafraa 04f2ab7ea9 Task #527: Unify topbar prefs into per-user Settings panel
- New `settings-panel.tsx` consolidates four duplicate topbar buttons
  (clock-style picker, quick-mute, notification-settings popover,
  language-toggle globe) behind a single gear icon that opens a
  right-side Sheet (full-width on mobile, capped on desktop).
- Panel groups: Language (ar/en), Notifications (per-slot
  enabled/sound/vibration), Sound (master mute), Clock (visibility,
  12/24h, style).
- Reuses existing hooks (useUpdateLanguage, useUpdate{Clock*},
  useUpdatePrefs, useHome/TopbarClockVisibility) so all persistence
  paths (DB columns + localStorage) stay identical — no migration.
- home.tsx topbar now renders only inbox link, single notification
  bell (kept as nav link), Settings gear, and logout.
- New i18n keys under `settingsPanel.*` in ar.json + en.json.
- Old ClockStylePicker / NotificationSettingsPicker / QuickMuteButton
  exports remain in their original files (unused) — kept to minimize
  diff scope; can be removed in a follow-up.
2026-05-13 14:46:50 +00:00
riyadhafraa 143ad9a29d Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project
can be cloned and run on any Linux VPS with `docker compose up`.

Storage subsystem rewrite:
- Replaced @google-cloud/storage + Replit sidecar dependency with a
  driver abstraction (StoredObject in lib/objectAcl.ts) and two
  implementations: LocalDriver (filesystem + HMAC-signed PUT route at
  /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint
  via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected
  by STORAGE_DRIVER / S3_ENDPOINT env vars.
- Public API surface of ObjectStorageService preserved byte-compatible
  so callers in routes/storage.ts and routes/executive-meetings.ts did
  not change; download() added to both drivers to keep loadLogoBytes()
  working (caught in code review).
- Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in
  test C) all pass against the new local driver. Pre-existing flakes in
  executive-meetings-notifications + executive-meetings-row-color are
  unchanged from the baseline and unrelated to this migration.

Infrastructure:
- Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses
  the official Playwright base image so PDF rendering works in-container;
  web stage is nginx serving the Vite SPA bundle.
- docker-compose.yml: postgres + minio + minio-init (creates buckets) +
  api + web + one-shot migrate runner. Healthchecks on every long-lived
  service.
- docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket
  upgrade ordering.
- .env.example: every runtime env var documented with comments.
- README.md replaces replit.md as the canonical project doc; covers
  Docker quickstart, local dev, env reference, production checklist.
- MIGRATION_REPORT.md: file-by-file diff of what changed and why.

Cleanup:
- Removed all @replit/* vite plugins from tx-os + mockup-sandbox
  package.json + vite.config.ts + pnpm-workspace.yaml catalog.
- Removed @google-cloud/storage and google-auth-library from api-server.
- Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and
  *.tsbuildinfo build artefacts, scripts/post-merge.sh.
- Stripped Replit references from threat_model.md (now describes the
  self-hosted topology).
- New comprehensive .gitignore: Replit configs (.replit, replit.nix,
  replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/,
  .upm/), local storage/, .env*, build artefacts. .replit and replit.nix
  remain on disk (sandbox-protected) but will not ship to GitHub.
- scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD
  from env and throws in production if either is unset.

Drift from plan: replit.md was deleted (per off-Replit scope) so its
"do-not-touch files" preference list is moot. .replit/replit.nix kept on
disk only because they are sandbox-protected from edit/delete in this
environment, but they are git-ignored so they will not appear in a
fresh clone.
2026-05-13 14:08:11 +00:00
riyadhafraa d2b4f82736 Task #521: shorten meetings page URL to /meetings
- artifacts/tx-os/src/App.tsx: serve the schedule at /meetings; add an
  ExecutiveMeetingsRedirect that rewrites /executive-meetings (and any
  /executive-meetings/* sub-path) to /meetings while preserving the
  query string and hash, so existing PDFs, emails, and bookmarks keep
  working with one transparent hop.
- scripts/src/seed.ts: seed the apps row with route="/meetings" and
  update the expectedBuiltinRoutes drift guard to match. Add an
  idempotent UPDATE-by-slug after the apps insert so already-deployed
  rows (including any drift like /executive-meetings or /mms) get
  reconciled to /meetings on the next seed — safe because the slug is
  in BUILTIN_APP_SLUGS, meaning the SPA owns the route.
- artifacts/tx-os/tests/meetings-route-redirect.spec.mjs: new spec
  verifying that /meetings loads, /executive-meetings?date=...#... is
  redirected with query+hash intact, and /api/apps now reports
  route="/meetings" for the executive-meetings slug.

Out of scope (intentionally left untouched per task): API paths under
/api/executive-meetings/*, React Query keys, DB tables, the apps slug
"executive-meetings", page/component file names, the "Meetings" display
name, and the executive_meetings:* permission name.
2026-05-13 07:06:14 +00:00
riyadhafraa bfbf59cb20 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:52:11 +00:00
riyadhafraa 8b3c15ba21 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:50:45 +00:00
riyadhafraa 4a48614e43 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:49:20 +00:00
riyadhafraa 9d60b35955 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:47:44 +00:00
riyadhafraa 6a01ce852a Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required when an external mode is chosen.
- Internal route field is hidden entirely when an external mode is
  selected, and locked (readOnly + lock hint) when editing a built-in
  app. Slug input is also locked (readOnly) for built-in apps so the
  built-in identity cannot drift via the form.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug. Exposed via subpath export
  `@workspace/db/built-in-apps`; the file has zero imports so the
  browser bundle uses it without pulling in `pg`. tx-os now imports it
  directly — duplicate FE constant removed.
- Built-in slug list: services, notifications, admin, notes,
  my-orders, orders-incoming, executive-meetings (everything with a
  hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar /
  documents are seeded but admin-defined and remain editable.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.
- PATCH /apps/:id ALSO rejects slug changes when the previous slug is
  built-in (code='builtin_slug_locked'). UpdateAppBody zod schema
  intentionally omits slug, so we inspect req.body.slug raw before zod
  stripping. Closes the 2-step bypass: rename slug (allowed) → change
  route (now previous.slug looks non-built-in, allowed).
- POST /apps and PATCH /apps/:id reject externalUrl values that are
  not http:// or https:// (code='invalid_external_url'). Prevents
  shipping javascript:/data:/file: payloads tenant-wide via launcher.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- scripts/src/seed.ts: drift guard throws if a seeded built-in slug
  uses a route that does not match the hardcoded SPA route.

Tests:
- New API test apps-builtin-route-lock.test.mjs (6/6 pass): reject
  built-in route change, allow non-route built-in updates, allow
  non-builtin route changes, reject built-in slug change (anti-bypass),
  reject non-http(s) externalUrl scheme + accept https, allow built-in
  same-route no-op.
- New Playwright E2E admin-app-image-external-embedded.spec.mjs
  (passes): launcher renders custom image_url, external_tab opens
  external URL via window.open, external_iframe navigates to
  /embedded/:id and renders <iframe src=externalUrl>.

Out of scope (pre-existing, not introduced here):
- Failing tests in executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts.
2026-05-12 12:37:34 +00:00
riyadhafraa e7948012f4 Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required when an external mode is chosen.
  Internal route field is hidden entirely when an external mode is
  selected, and locked (readOnly + lock hint) when editing a built-in
  app whose path is hardcoded in the SPA.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug. Exposed via subpath export
  `@workspace/db/built-in-apps`; the file has zero imports so the
  browser bundle can use it without pulling in `pg`. tx-os now imports
  it directly — duplicate FE constant removed.
- Built-in slug list: services, notifications, admin, notes,
  my-orders, orders-incoming, executive-meetings (everything with a
  hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar /
  documents are seeded but admin-defined and remain editable.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- scripts/src/seed.ts: drift guard throws if a seeded built-in slug
  uses a route that does not match the hardcoded SPA route.

Tests:
- New API test apps-builtin-route-lock.test.mjs (4/4 pass): reject
  built-in route change, allow non-route built-in updates, allow
  non-builtin route changes, allow built-in same-route no-op.
- New Playwright E2E admin-app-image-external-embedded.spec.mjs
  (passes): launcher renders custom image_url, external_tab opens
  external URL via window.open, external_iframe navigates to
  /embedded/:id and renders <iframe src=externalUrl>.

Out of scope (pre-existing, not introduced here):
- 3 failing tests in executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts.
2026-05-12 12:31:43 +00:00
riyadhafraa a794f92e61 Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required by the form when an external mode
  is chosen.
- Route field is locked (readOnly + lock hint) when editing a built-in
  app, since those slugs are hardcoded in the SPA router.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug, re-exported from lib/db.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- New tests in apps-builtin-route-lock.test.mjs (4/4 pass) covering
  reject built-in route change, allow non-route built-in updates,
  allow non-builtin route changes, allow built-in same-route no-op.

Notes / drift:
- BUILTIN_APP_SLUGS is duplicated inline in admin.tsx
  (BUILTIN_APP_SLUGS_FE) because the browser bundle cannot import
  @workspace/db (pulls pg). Comment points at the canonical source;
  drift risk filed as a follow-up.
- Pre-existing failures unrelated to this task: 3 tests in
  executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts. Out of scope.
2026-05-12 12:23:38 +00:00
riyadhafraa f62969dc53 Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to
/notes?thread=X&reply=1, yanking the user out of whatever page they
were on (Tasks, Calendar, etc.). Now Reply opens an inline composer
inside the popup card itself.

Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx):
- New composerOpen / replyText / justSent state, reset whenever the
  popup head rotates so a half-typed reply never leaks across payloads.
- handleReply now opens the inline composer and focuses the textarea
  (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to
  successful send so Cancel keeps the note unread.
- handleSubmitReply uses the existing useReplyToNote hook. Resolves
  recipientUserId from replyPayload.replier.id for the reply variant
  (owner replying back), undefined for the note variant (recipient
  replying — server infers the original sender).
- On success: brief on-card "Reply sent" confirmation (aria-live),
  then auto-dismiss after 700ms. On failure: destructive toast with
  the textarea preserved.
- Composer textarea: Enter (or Cmd/Ctrl+Enter) sends, Shift+Enter
  inserts a newline. Esc cancels the composer and is handled by the
  popup's global keydown listener so it works whether focus is in the
  textarea or on the Send/Cancel buttons. dir="auto" so the user's
  reply renders RTL/LTR per its own characters.
- pointerdown/click stopPropagation on the composer wrapper so typing
  doesn't accidentally drag the card.

Locale keys added to ar.json + en.json under notes.popup:
inlineReplyPlaceholder, sendReply, cancelReply, replySending,
replySent, replyFailed.

Pre-existing failing tests (groups-crud, executive-meetings-
notifications, executive-meetings-postpone-race) are unrelated to
this change.
2026-05-12 11:58:37 +00:00
riyadhafraa a55fb16c4c Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to
/notes?thread=X&reply=1, yanking the user out of whatever page they
were on (Tasks, Calendar, etc.). Now Reply opens an inline composer
inside the popup card itself.

Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx):
- New composerOpen / replyText / justSent state, reset whenever the
  popup head rotates so a half-typed reply never leaks across payloads.
- handleReply now opens the inline composer and focuses the textarea
  (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to
  successful send so Cancel keeps the note unread.
- handleSubmitReply uses the existing useReplyToNote hook. Resolves
  recipientUserId from replyPayload.replier.id for the reply variant
  (owner replying back), undefined for the note variant (recipient
  replying — server infers the original sender).
- On success: brief on-card "Reply sent" confirmation (aria-live),
  then auto-dismiss after 700ms. On failure: destructive toast with
  the textarea preserved.
- Composer textarea: Cmd/Ctrl+Enter to send, plain Enter for newline,
  Esc to cancel (stops propagation so the global Esc listener doesn't
  dismiss the whole popup with an unsent reply). dir="auto" so the
  user's reply renders RTL/LTR per its own characters.
- pointerdown/click stopPropagation on the composer wrapper so typing
  doesn't accidentally drag the card.

Locale keys added to ar.json + en.json under notes.popup:
inlineReplyPlaceholder, sendReply, cancelReply, replySending,
replySent, replyFailed.

Pre-existing failing tests (groups-crud, executive-meetings-
notifications, executive-meetings-postpone-race) are unrelated to
this change.
2026-05-12 11:57:08 +00:00
riyadhafraa b7b8c395f4 Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Switched the folders+content row in
  `artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid:
  `grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in
  col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is
  shown), content in col 2 row 2. Result: composer's top edge aligns
  with the rail's top edge on desktop and the empty gap is gone.
- On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the
  exact original mobile reading order: composer → rail → content.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root `<aside>`, so the page can pass grid placement
  classes.
- Hidden-composer views (Inbox / Sent / Archived / Shared-with-me)
  collapse cleanly via conditional `md:row-span-2` on the rail and
  conditional `md:row-start-1 / md:row-start-2` on the content
  column — desktop becomes a simple two-cell row and mobile stays
  rail → content, matching the original behavior.
- Grid-cols template, `md:col-start-2` on composer, and
  `md:col-start-2`/`md:row-start-*` on the content column are all
  GATED on `showFolders`. In views where the folders rail is hidden
  (Received/Inbox, Sent), the wrapper is a single-column grid with
  no left gutter and content takes the full width — preserving the
  original full-width layout for those views.

Behavior preserved:
- `showTopComposer` gating unchanged.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing internal classes unchanged; new className
  prop is purely additive.
- Mobile reading order in active/non-shared view: composer → rail →
  content (same as before this task, when composer was a standalone
  block above the row).
- Mobile reading order in hidden-composer views: rail → content
  (unchanged).

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:31:37 +00:00
riyadhafraa cb6b067386 Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Switched the folders+content row in
  `artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid:
  `grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in
  col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is
  shown), content in col 2 row 2. Result: composer's top edge aligns
  with the rail's top edge on desktop and the empty gap is gone.
- On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the
  exact original mobile reading order: composer → rail → content.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root `<aside>`, so the page can pass grid placement
  classes.
- Hidden-composer views (Inbox / Sent / Archived / Shared-with-me)
  collapse cleanly via conditional `md:row-span-2` on the rail and
  conditional `md:row-start-1 / md:row-start-2` on the content
  column — desktop becomes a simple two-cell row and mobile stays
  rail → content, matching the original behavior.

Behavior preserved:
- `showTopComposer` gating unchanged.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing internal classes unchanged; new className
  prop is purely additive.
- Mobile reading order in active/non-shared view: composer → rail →
  content (same as before this task, when composer was a standalone
  block above the row).
- Mobile reading order in hidden-composer views: rail → content
  (unchanged).

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:30:09 +00:00
riyadhafraa d1875141aa Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Moved the top composer out of its standalone full-width block and
  into the left content column (`flex-1 min-w-0`) of the existing
  folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both
  now share the same row, so the composer's top edge aligns with the
  rail's top edge on desktop.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root <aside>, so the page can pass `order-*` classes.
- On narrow widths (`flex-col`) the composer column gets
  `order-1 md:order-2` and the rail gets `order-2 md:order-1` ONLY
  when `showTopComposer` is true. In hidden-composer views (Inbox /
  Sent / Archived / Shared-with-me) no order classes are applied, so
  the rail/content stacking order on mobile stays exactly as it was
  before this task. Result: composer stacks ABOVE the rail on mobile
  in My Notes (preserving the original mobile reading order) and
  sits beside it on md+, with zero behavior change in the other
  views.

Behavior preserved:
- `showTopComposer` gating unchanged — still hidden on Inbox / Sent /
  Archived / Shared-with-me.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing width/scroll classes unchanged; new className
  is purely additive.
- Stale comment about composer being "above the folders area" updated
  to describe the new in-column placement and the order-* mobile
  behavior.

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:27:10 +00:00
riyadhafraa 944a894a88 Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Moved the top composer out of its standalone full-width block and
  into the left content column (`flex-1 min-w-0`) of the existing
  folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both
  now share the same row, so the composer's top edge aligns with the
  rail's top edge on desktop.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root <aside>, so the page can pass `order-*` classes.
- On narrow widths (`flex-col`) the composer column gets
  `order-1 md:order-2` and the rail gets `order-2 md:order-1`. Result:
  composer stacks ABOVE the rail on mobile (preserving the original
  mobile reading order) and sits beside it on md+.

Behavior preserved:
- `showTopComposer` gating unchanged — still hidden on Inbox / Sent /
  Archived / Shared-with-me.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing width/scroll classes unchanged; new className
  is purely additive.
- Stale comment about composer being "above the folders area" updated
  to describe the new in-column placement and the order-* mobile
  behavior.

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:25:45 +00:00
riyadhafraa 037d530471 Task #514: Align note composer with folders rail
Original task: On the Notes page, the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below it. The result was a visible
vertical mismatch — composer high, rail noticeably lower with empty
space between them.

Change: Moved the top composer out of its standalone full-width block
and into the left content column (`flex-1 min-w-0`) at the top of the
existing row that already hosts the FoldersRail. Both now share the
same row, so the composer's top edge aligns with the rail's top edge.

Details:
- Single edit in `artifacts/tx-os/src/pages/notes.tsx`.
- The composer's `data-testid="notes-top-composer"` is preserved, so
  any existing E2E selectors continue to work.
- `showTopComposer` gating (hide on Inbox/Sent/Archived/Shared-with-me)
  is unchanged — only the position of the rendered block moved.
- Removed the old wrapper's `px-4 pt-1 max-w-6xl w-full mx-auto`
  (those paddings/centering are now provided by the parent row), and
  added a `mb-4` for breathing room between composer and the content
  below it inside the column.
- Responsive behavior preserved: the parent row is
  `flex-col md:flex-row`, so on narrow widths the composer still
  stacks above the folders area cleanly with no horizontal overflow.

Verification: pnpm tsc --noEmit on tx-os passes; no test files
reference the composer block by structural selectors that the move
would break.

No deviations from the plan.
2026-05-12 11:22:47 +00:00
riyadhafraa 9a8dc3175f Task #512: Per-recipient Delete in Notes Inbox
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.

Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
  single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
  first.

Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
  src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar: Delete button (rose) next to Archive plus a confirm
  AlertDialog with all/none/partial toasts.
- Inbox card: per-row Trash button next to the status badge with a
  page-level confirm AlertDialog (testid inbox-row-delete-confirm).
- ThreadDialog: per-row Delete next to Archive plus a confirm dialog
  that closes the thread on success.
- AR + EN locale strings added for all new copy.

Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
  delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
  DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
  flow seeds three received notes, bulk-deletes two from the inbox,
  then deletes the third via the per-row card Delete button.
2026-05-12 11:18:00 +00:00
riyadhafraa 0988585c65 Task #512: Per-recipient Delete in Notes Inbox
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.

Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
  single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
  first.

Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
  src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar gets a Delete button (rose) next to Archive plus a
  confirm AlertDialog with all/none/partial toasts (src/pages/notes.tsx).
- ThreadDialog gets a per-row Delete next to Archive plus a single
  confirm dialog that closes the thread on success.
- AR + EN locale strings added for all new copy.

Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
  delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
  DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
  flow seeds three received notes, bulk-deletes two from the inbox,
  then deletes the third via ThreadDialog per-row.
2026-05-12 11:14:33 +00:00
riyadhafraa 84398de390 Task #511: Fully remove Chat feature
Destructive removal per user confirmation ("حذف نهائي ما يرجع").

Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
  socket handlers in src/index.ts, /admin/users/:id/dependents/
  conversations+messages endpoints, conversation/message dependency
  counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
  icon and messages StatCard on home, all chat-related UI in
  notifications + admin (dependency badges, delete-dialog rows,
  UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
  admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
  admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
  paths, related schemas (ConversationWithDetails, MessageWithSender,
  UserDependentConversation/MessageItem+Page, etc.), UserProfile and
  UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
  Regenerated client via orval.
- Database: dropped message_reads, messages,
  conversation_participants, conversations (CASCADE); deleted
  notifications with related_type='conversation' or type='chat';
  deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
  seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
  list-dependency-counts, delete-force-warnings, audit-log-coverage,
  and admin-inline-dependency-counts (e2e) — replaced chat dependents
  with note dependents where needed for force-delete coverage.

Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.

executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
2026-05-12 10:51:31 +00:00
riyadhafraa 992f98418c Add ability to archive multiple incoming notes at once
Introduces a bulk archive feature for the received notes tab, including API integration, UI selection state, confirmation dialogs, and internationalization support.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7c13c05c-e8c2-461e-a5dc-94bbc8e114ac
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv
Replit-Helium-Checkpoint-Created: true
2026-05-12 10:25:41 +00:00
riyadhafraa 207d2ad52b Improve Arabic text display by fixing clipped dots on characters
Add a global CSS rule to increase line-height for truncated Arabic text, ensuring diacritics are fully visible.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8f7b38c9-dbac-45e9-86d9-0cb4f4a5d011
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv
Replit-Helium-Checkpoint-Created: true
2026-05-12 09:47:05 +00:00
riyadhafraa e4b5ece2e4 Enhance the app editing interface with improved input hints
Add helper text and inline hints to the slug and route input fields within the app editing form to clarify their purpose and expected format.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 18fca138-6c4d-4f2b-9859-dcd578b8df5c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv
Replit-Helium-Checkpoint-Created: true
2026-05-12 09:36:50 +00:00
riyadhafraa d5dd3acf9f Task #509: Redesign Edit App modal + verify responsive sizing
User asked for the Edit App modal to match the new professional Edit
Service design and confirmed responsiveness for iPad / mobile.

Changes — admin.tsx only:

1. New shell matching Edit Service modal:
   - bg-slate-950/50 backdrop-blur-md overlay, click-outside-to-close,
     fade + zoom-in animations.
   - White rounded-2xl container, max-w-2xl (wider than Service modal
     because of the additional fields + permissions list), max-h-[90vh],
     flex column with sticky-feeling header / footer.
   - Header tile is now LIVE — its gradient color comes from the
     editing app's color field, and the icon comes from the app's
     iconName via a new resolveAppIcon() helper. So the header
     previews the app as you edit.
   - X close button in header.

2. Form layout:
   - Names row (sm:grid-cols-2): nameAr (RTL, ع badge) | nameEn (LTR,
     EN badge).
   - Identifier row (sm:grid-cols-2): slug | route, both font-mono.
   - Visuals row (sm:grid-cols-2):
     - Icon picker: 40x40 preview tile + text input. Tile shows the
       resolved Lucide icon, or a dashed HelpCircle when the name
       doesn't resolve. Hint text under the field.
     - Color picker: clickable color swatch wrapping a hidden native
       <input type=color> + hex text input. Both stay in sync.
   - Permissions wrapped in a bordered card with its own header strip.

3. Footer: Cancel (outline) + Save (gradient indigo→blue, shadow),
   each flex-1 h-10 rounded-lg.

4. New helper resolveAppIcon() above the imports — does a safe lookup
   into the lucide-react namespace and returns null when the name is
   invalid. Mirrors the pattern already used in pages/home.tsx.

Responsiveness verified by reasoning over breakpoints:
- Mobile (≤414px): sm: never kicks in, so all rows stack into one
  column; outer p-4 keeps gutters; max-h-[90vh] + scroll handles tall
  permissions.
- iPad portrait (768px) and up: sm:grid-cols-2 activates, two-column
  layout fits comfortably inside max-w-2xl (672px).
- Desktop: same max-w-2xl cap looks well-proportioned, not cramped.
- Edit Service modal already uses the same primitives, no changes
  needed.

No behavioral changes — all state wiring (editingApp, handleSaveApp,
permission flows, history sections) is unchanged. Pure presentational
refactor.

Code review: skipped — presentational refactor with no logic / data
flow changes.
Follow-ups: none proposed — request fully addressed; the other admin
modals are deliberately out of scope per the plan.
2026-05-12 09:35:47 +00:00
riyadhafraa c6fa183203 Redesign service editing window for a professional look and feel
Refactors the "Edit Service" modal UI in `admin.tsx` to improve visual hierarchy, user experience, and overall professionalism, including styling changes to the header, input fields, image uploader, and availability toggle.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 28ee2705-c2ec-476e-aa4f-cc6c7cd5624e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv
Replit-Helium-Checkpoint-Created: true
2026-05-12 09:27:57 +00:00
riyadhafraa a565d47027 Task #508: Remove unused السعر (Price) field from admin Services UI
User requested removal of the price field from the admin Services edit
form ("بالنسبه الى الاسعار أحذفها لايوجد اسعار") because there are no
prices in the product.

Changes — frontend only, all in tx-os:
- artifacts/tx-os/src/pages/admin.tsx
  - Drop `price: string` from ServiceForm type.
  - Drop `price: "0.00"` from emptyServiceForm.
  - Remove the price entry from the edit dialog's field array
    (no more السعر input).
  - Remove the `<div>{service.price}</div>` from the services list.
  - Remove the `price` line from the edit-load mapper.
- artifacts/tx-os/src/locales/ar.json + en.json
  - Drop the now-unused `servicePrice` key.

Left intact:
- The generic `"price"` key in locales (line 104) — kept conservatively
  in case anything outside this scope expects it, though ripgrep shows
  no current usage.
- The `services.price` DB column and api-server code — unused in api
  src; only test fixtures insert a placeholder. Removing would require
  a migration and break tests for zero benefit.
- The user-facing services page — never displayed price.

Verification:
- ripgrep confirms zero `price` / `servicePrice` references remain in
  artifacts/tx-os/src/pages/admin.tsx and the locales (other than the
  generic shared key).
- Vite HMR applied cleanly with no TS errors.
- Pre-existing test failures (executive-meetings, service-orders) are
  unrelated and already tracked as separate project tasks.

Code review: skipped — change is a 5-line mechanical removal of dead
UI with no logic or data-flow implications.

Follow-ups: none proposed — this is a self-contained, complete fix.
2026-05-12 09:16:52 +00:00
riyadhafraa b87f9224a7 Task #507: Stop .os-bg from overriding sticky on its direct children
User report: after #505 the admin "لوحة التحكم" header looked pinned
at the top of the page, but disappeared as soon as the user scrolled
down. Same bug silently affected every page wrapped in `.os-bg`.

Root cause (artifacts/tx-os/src/index.css, lines 190-193):

  .os-bg > * { position: relative; z-index: 1; }

This rule was meant to lift page content above the decorative
gradient blobs (`.os-bg::before/::after`, position:fixed z-index:0).
But by setting `position: relative` on every direct child it also
overrode any `position: sticky` declared via Tailwind utilities on
those children — equal-specificity class selectors, with index.css
loaded after Tailwind utilities, so .os-bg won source-order. The
"sticky" headers therefore behaved as plain in-flow elements and
scrolled away with the rest of the content.

Fix (one CSS rule):
- Replace `position: relative; z-index: 1` with `isolation: isolate`.
- `isolation: isolate` creates a new stacking context (so children
  still paint above the z-index:0 blobs) without forcing them to
  position:relative — sticky/fixed children now work as written.

No markup changes anywhere. Followed Option A in the task plan
exactly. Implication: every sticky header across the app (admin,
home, chat, services, notifications) is now actually pinned on
scroll. Decorative blobs continue to sit behind page content
because the new stacking context still paints over the parent's
z-index 0 background layer.

No tests were modified; existing tx-os tests are presentation-
agnostic and unaffected.
2026-05-12 09:06:24 +00:00