Commit Graph

2249 Commits

Author SHA1 Message Date
Riyadh 356b8d3ddb Task #160: 12-hour time display for executive meetings
Switch the executive-meetings schedule time display from 24-hour
(e.g. "23:47 – 15:15") to 12-hour locale-aware format with Latin
digits in both languages:
  - EN: "11:47 PM – 3:15 PM"
  - AR: "11:47 م – 3:15 م"

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  * Local formatTime() now accepts (t, lang) and routes through the
    shared i18nFormatTime helper with hour12: true.
  * TimeRangeCell now takes a lang: "ar" | "en" prop, threaded down
    from MeetingRow (isRtl ? "ar" : "en"). Fixes the runtime
    "ReferenceError: lang is not defined" from the previous attempt.
  * ManageSection list passes (isRtl ? "ar" : "en") inline.
- artifacts/tx-os/src/pages/executive-meetings-print.tsx
  * New formatPrintTime() mirrors the same conversion. Both render
    branches (full range, start-only) updated.

Out of scope (kept untouched per the plan):
- <input type="time"> form fields (still HH:mm 24h, HTML standard).
- DB schema, API, generated client.
- User clockHour12 preference, home clock, chat — all still honor
  their existing per-user setting.

Verified: tx-os tsc clean. Architect review: PASS.
2026-04-29 14:57:50 +00:00
riyadhafraa 2982a28432 Task #160: 12-hour time display for executive meetings
Switch the executive-meetings schedule time display from 24-hour
(e.g. "23:47 – 15:15") to 12-hour locale-aware format with Latin
digits in both languages:
  - EN: "11:47 PM – 3:15 PM"
  - AR: "11:47 م – 3:15 م"

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  * Local formatTime() now accepts (t, lang) and routes through the
    shared i18nFormatTime helper with hour12: true.
  * TimeRangeCell now takes a lang: "ar" | "en" prop, threaded down
    from MeetingRow (isRtl ? "ar" : "en"). Fixes the runtime
    "ReferenceError: lang is not defined" from the previous attempt.
  * ManageSection list passes (isRtl ? "ar" : "en") inline.
- artifacts/tx-os/src/pages/executive-meetings-print.tsx
  * New formatPrintTime() mirrors the same conversion. Both render
    branches (full range, start-only) updated.

Out of scope (kept untouched per the plan):
- <input type="time"> form fields (still HH:mm 24h, HTML standard).
- DB schema, API, generated client.
- User clockHour12 preference, home clock, chat — all still honor
  their existing per-user setting.

Verified: tx-os tsc clean. Architect review: PASS.
2026-04-29 14:57:50 +00:00
riyadhafraa abea383133 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f8a14ccf-b332-489e-bf3d-b1dfa1263e7e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/CMw20Ap
Replit-Helium-Checkpoint-Created: true
2026-04-29 14:52:25 +00:00
Riyadh c58e0f10ff Add automated tests for assigning permissions to a role (Task #107)
Adds artifacts/api-server/tests/role-permissions-assign.test.mjs covering
the previously-uncovered role permission-assignment endpoints in
artifacts/api-server/src/routes/roles.ts:

- PUT /api/roles/:id/permissions
  - Replaces an existing set (verifies returned body and the row set in
    role_permissions in the DB).
  - System-role guard: returns 400 with code "system_role_permissions"
    and leaves the system role's permissions unchanged.
  - Unknown permission id: returns 404 and the rejected PUT does NOT
    partially apply (DB set unchanged).

- POST /api/roles/:id/permissions
  - Adds a single permission, returns 201, and is idempotent on repeat
    add (no duplicate row, still 201).
  - System-role guard: 400 with code "system_role_permissions",
    DB unchanged.
  - Unknown permission id: 404, DB unchanged.

- DELETE /api/roles/:id/permissions/:permissionId
  - Removes the permission and returns 204; idempotent on second delete.
  - Idempotent (204, not 404) for an unknown permission id; the role's
    other permissions are not collaterally removed.
  - System-role guard: 400 with code "system_role_permissions",
    DB permissions for system role unchanged.

Style mirrors tests/roles-crud.test.mjs and role-permission-audit.test.mjs:
test admin user provisioned in `before`, login via /api/auth/login, full
cleanup of created roles/users in `after`. Tests run via the standard
`pnpm --filter @workspace/api-server test` command.

Verified all 8 new tests pass against the running API server. The two
pre-existing failures in app-permissions-unique.test.mjs are unrelated
and already tracked by a separate task.
2026-04-29 14:42:26 +00:00
riyadhafraa 479fa00ecc Add automated tests for assigning permissions to a role (Task #107)
Adds artifacts/api-server/tests/role-permissions-assign.test.mjs covering
the previously-uncovered role permission-assignment endpoints in
artifacts/api-server/src/routes/roles.ts:

- PUT /api/roles/:id/permissions
  - Replaces an existing set (verifies returned body and the row set in
    role_permissions in the DB).
  - System-role guard: returns 400 with code "system_role_permissions"
    and leaves the system role's permissions unchanged.
  - Unknown permission id: returns 404 and the rejected PUT does NOT
    partially apply (DB set unchanged).

- POST /api/roles/:id/permissions
  - Adds a single permission, returns 201, and is idempotent on repeat
    add (no duplicate row, still 201).
  - System-role guard: 400 with code "system_role_permissions",
    DB unchanged.
  - Unknown permission id: 404, DB unchanged.

- DELETE /api/roles/:id/permissions/:permissionId
  - Removes the permission and returns 204; idempotent on second delete.
  - Idempotent (204, not 404) for an unknown permission id; the role's
    other permissions are not collaterally removed.
  - System-role guard: 400 with code "system_role_permissions",
    DB permissions for system role unchanged.

Style mirrors tests/roles-crud.test.mjs and role-permission-audit.test.mjs:
test admin user provisioned in `before`, login via /api/auth/login, full
cleanup of created roles/users in `after`. Tests run via the standard
`pnpm --filter @workspace/api-server test` command.

Verified all 8 new tests pass against the running API server. The two
pre-existing failures in app-permissions-unique.test.mjs are unrelated
and already tracked by a separate task.

Replit-Task-Id: 885dca7a-421d-4be1-993b-299bfd0719b4
2026-04-29 14:42:26 +00:00
Riyadh e740d58470 Add Playwright tests for the My Orders cancel/delete undo toast
Original task (#103): cover the 7-second "Undo" action that appears after
cancelling or deleting an order on /my-orders, since the existing
order-place-cancel spec only verified the post-cancel state and never
exercised the undo path.

What was added:
- New spec at artifacts/tx-os/tests/order-undo-toast.spec.mjs with two cases:
  1. English: place a fresh pending order via the services modal, cancel it
     from the in-card confirm dialog, click "Undo" inside the toast within
     the 7-second window, then assert the restore PATCH /api/orders/:id/status
     succeeds, the timeline + Cancel button reappear in the card, and the DB
     row is back to "pending".
  2. Arabic: seed a "completed" order directly in the DB, click the trash
     icon + "نعم، احذف", then click "تراجع" in the toast. Verifies that NO
     DELETE /api/orders/:id ever fires (page-level request listener), the
     order row still exists in the DB after the original 7s window has
     elapsed, and the card stays visible with the مكتمل status pill.
- Both English and Arabic toast labels are exercised, satisfying the
  bilingual requirement.

Implementation notes / deviations:
- The spec is automatically picked up by the existing test:e2e config
  (testMatch: /.*\.spec\.mjs$/), so no playwright.config or package.json
  changes were needed.
- DB seeding/cleanup mirrors the conventions in
  artifacts/tx-os/tests/order-place-cancel.spec.mjs (same bcrypt hash for
  TestPass123!, same afterAll teardown of orders/notifications/user_roles/users).
- While wiring the test I discovered the shadcn Toaster's <li> root has no
  role="status" on this Radix version, so role-based selectors miss it.
  The spec uses `li[data-state="open"]` (Radix's data attribute) scoped to
  the most recent toast — explained in a comment in the file.

Verification:
- pnpm exec playwright test tests/order-undo-toast.spec.mjs → 2 passed.
- Re-ran together with the existing order-place-cancel spec → 4 passed,
  no regressions.

Follow-ups proposed (#157, #158): bulk "Clear N finished" + undo coverage,
and the unmount-flushes-pending-deletes path.
2026-04-29 14:28:30 +00:00
riyadhafraa 9b3e320c78 Add Playwright tests for the My Orders cancel/delete undo toast
Original task (#103): cover the 7-second "Undo" action that appears after
cancelling or deleting an order on /my-orders, since the existing
order-place-cancel spec only verified the post-cancel state and never
exercised the undo path.

What was added:
- New spec at artifacts/tx-os/tests/order-undo-toast.spec.mjs with two cases:
  1. English: place a fresh pending order via the services modal, cancel it
     from the in-card confirm dialog, click "Undo" inside the toast within
     the 7-second window, then assert the restore PATCH /api/orders/:id/status
     succeeds, the timeline + Cancel button reappear in the card, and the DB
     row is back to "pending".
  2. Arabic: seed a "completed" order directly in the DB, click the trash
     icon + "نعم، احذف", then click "تراجع" in the toast. Verifies that NO
     DELETE /api/orders/:id ever fires (page-level request listener), the
     order row still exists in the DB after the original 7s window has
     elapsed, and the card stays visible with the مكتمل status pill.
- Both English and Arabic toast labels are exercised, satisfying the
  bilingual requirement.

Implementation notes / deviations:
- The spec is automatically picked up by the existing test:e2e config
  (testMatch: /.*\.spec\.mjs$/), so no playwright.config or package.json
  changes were needed.
- DB seeding/cleanup mirrors the conventions in
  artifacts/tx-os/tests/order-place-cancel.spec.mjs (same bcrypt hash for
  TestPass123!, same afterAll teardown of orders/notifications/user_roles/users).
- While wiring the test I discovered the shadcn Toaster's <li> root has no
  role="status" on this Radix version, so role-based selectors miss it.
  The spec uses `li[data-state="open"]` (Radix's data attribute) scoped to
  the most recent toast — explained in a comment in the file.

Verification:
- pnpm exec playwright test tests/order-undo-toast.spec.mjs → 2 passed.
- Re-ran together with the existing order-place-cancel spec → 4 passed,
  no regressions.

Follow-ups proposed (#157, #158): bulk "Clear N finished" + undo coverage,
and the unmount-flushes-pending-deletes path.

Replit-Task-Id: bd1c5067-e994-4299-b3a6-855ffb45ed71
2026-04-29 14:28:30 +00:00
Riyadh 459c5304a0 Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule:

1. Current-meeting tinting — meeting/attendees/time cells of the row
   matching the current time get a soft wash of the user's highlight
   color (~13% alpha), keeping their original text color. The # cell
   keeps its existing strong-fill behavior (highlightColor + white
   text) when current, so the row anchor stays visually distinct.

2. Cell-merge overlay — editors can merge "meeting+attendees",
   "meeting+attendees+time", or the whole row into a single free-text
   cell. Stored in the DB so all viewers see the same overlay; the
   originals (titleAr/En, attendees, start/endTime) stay untouched
   and are restored on Unmerge.

Implementation notes
- DB: 3 nullable columns added to executive_meetings
  (merge_start_column, merge_end_column, merge_text). Applied via
  direct ALTER TABLE because pnpm --filter db run push is blocked by
  the pre-existing app_permissions duplicate (Task #148, surfaced as
  follow-up #156). Schema file kept in sync.
- API: meetingPatchSchema accepts an optional `merge` field
  (`null | {start,end,text}`) with start<=end ordering and server-side
  sanitizeRichText on the text. Reuses the existing PATCH route.
- Frontend: new MergeMenu component + canonical-order range
  resolution so merges still render when boundary columns are hidden,
  and gracefully degrade to unmerged rendering (without losing DB
  state) when column reordering makes the visible span non-contiguous.
  Unmerge stays available whenever a stored merge exists, even in
  degraded state, and existing mergeText is preserved on re-merge.
  Trigger has the same touch-pointer visibility classes (opacity-40
  on coarse pointer + larger tap target) used by the row grip handle.
- i18n: en + ar keys under executiveMeetings.merge.*
- Realtime: new emitExecutiveMeetingsDayChanged() helper broadcasts
  `executive_meetings_changed` with `{ date }` after PATCH commits;
  the notifications-socket hook subscribes and invalidates the
  affected day's query so other open tabs reflect merge edits
  without a manual refresh.

Out of scope (deferred to follow-ups)
- API + e2e test coverage for the merge endpoint and UI flows (#154)
- Realtime emission for the remaining EM mutations — POST/DELETE/
  PUT/duplicate/reorder (#155)

Validation
- TypeScript compiles cleanly across the changed surfaces (existing
  pre-existing errors in api-zod / generated client are unrelated).
- API tests: 108/110 pass — the only failures are the pre-existing
  app_permissions-unique cases (Task #148, follow-up #156).
- Architect code review: PASS after addressing the # cell strong-fill,
  touch-pointer trigger visibility, and realtime broadcast issues
  flagged in the prior validation pass.
2026-04-29 14:13:07 +00:00
riyadhafraa 43aa0c72a0 Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule:

1. Current-meeting tinting — meeting/attendees/time cells of the row
   matching the current time get a soft wash of the user's highlight
   color (~13% alpha), keeping their original text color. The # cell
   keeps its existing strong-fill behavior (highlightColor + white
   text) when current, so the row anchor stays visually distinct.

2. Cell-merge overlay — editors can merge "meeting+attendees",
   "meeting+attendees+time", or the whole row into a single free-text
   cell. Stored in the DB so all viewers see the same overlay; the
   originals (titleAr/En, attendees, start/endTime) stay untouched
   and are restored on Unmerge.

Implementation notes
- DB: 3 nullable columns added to executive_meetings
  (merge_start_column, merge_end_column, merge_text). Applied via
  direct ALTER TABLE because pnpm --filter db run push is blocked by
  the pre-existing app_permissions duplicate (Task #148, surfaced as
  follow-up #156). Schema file kept in sync.
- API: meetingPatchSchema accepts an optional `merge` field
  (`null | {start,end,text}`) with start<=end ordering and server-side
  sanitizeRichText on the text. Reuses the existing PATCH route.
- Frontend: new MergeMenu component + canonical-order range
  resolution so merges still render when boundary columns are hidden,
  and gracefully degrade to unmerged rendering (without losing DB
  state) when column reordering makes the visible span non-contiguous.
  Unmerge stays available whenever a stored merge exists, even in
  degraded state, and existing mergeText is preserved on re-merge.
  Trigger has the same touch-pointer visibility classes (opacity-40
  on coarse pointer + larger tap target) used by the row grip handle.
- i18n: en + ar keys under executiveMeetings.merge.*
- Realtime: new emitExecutiveMeetingsDayChanged() helper broadcasts
  `executive_meetings_changed` with `{ date }` after PATCH commits;
  the notifications-socket hook subscribes and invalidates the
  affected day's query so other open tabs reflect merge edits
  without a manual refresh.

Out of scope (deferred to follow-ups)
- API + e2e test coverage for the merge endpoint and UI flows (#154)
- Realtime emission for the remaining EM mutations — POST/DELETE/
  PUT/duplicate/reorder (#155)

Validation
- TypeScript compiles cleanly across the changed surfaces (existing
  pre-existing errors in api-zod / generated client are unrelated).
- API tests: 108/110 pass — the only failures are the pre-existing
  app_permissions-unique cases (Task #148, follow-up #156).
- Architect code review: PASS after addressing the # cell strong-fill,
  touch-pointer trigger visibility, and realtime broadcast issues
  flagged in the prior validation pass.
2026-04-29 14:13:07 +00:00
Riyadh 19c8c146c0 Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule:

1. Current-meeting tinting — meeting/attendees/time cells of the row
   matching the current time get a soft wash of the user's highlight
   color (~13% alpha), keeping their original text color and the
   existing colored ring. The # cell stays solid white/red.

2. Cell-merge overlay — editors can merge "meeting+attendees",
   "meeting+attendees+time", or the whole row into a single free-text
   cell. Stored in the DB so all viewers see the same overlay; the
   originals (titleAr/En, attendees, start/endTime) stay untouched and
   are restored on Unmerge.

Implementation notes
- DB: 3 nullable columns added to executive_meetings
  (merge_start_column, merge_end_column, merge_text). Applied via
  direct ALTER TABLE because pnpm --filter db run push is blocked by
  the pre-existing app_permissions duplicate (Task #148, surfaced as
  the new follow-up #156). Schema file kept in sync.
- API: meetingPatchSchema accepts an optional `merge` field
  (`null | {start,end,text}`) with start<=end ordering and server-side
  sanitizeRichText on the text. Reuses the existing PATCH route.
- Frontend: new MergeMenu component + canonical-order range resolution
  so merges still render when boundary columns are hidden, and
  gracefully degrade to unmerged rendering (without losing DB state)
  when column reordering makes the visible span non-contiguous.
  Unmerge stays available whenever a stored merge exists, even in
  degraded state, and existing mergeText is preserved on re-merge.
- i18n: en + ar keys under executiveMeetings.merge.*

Out of scope (deferred to follow-ups #154 / #155, per task brief)
- API + e2e test coverage for the merge endpoint and UI flows
- Realtime emission for executive-meetings mutations (no existing
  emit on this surface; covered by #155)

Validation
- TypeScript compiles cleanly across api-server + tx-os.
- API tests: 108/110 pass — the 2 failures are the pre-existing
  app_permissions-unique cases (Task #148, follow-up #156),
  unrelated to this change.
- Architect code review approved after addressing hidden-column
  fallback, non-contiguous reorder degradation, and
  Unmerge-availability edge cases.
2026-04-29 14:05:21 +00:00
riyadhafraa 539f11e25d Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule:

1. Current-meeting tinting — meeting/attendees/time cells of the row
   matching the current time get a soft wash of the user's highlight
   color (~13% alpha), keeping their original text color and the
   existing colored ring. The # cell stays solid white/red.

2. Cell-merge overlay — editors can merge "meeting+attendees",
   "meeting+attendees+time", or the whole row into a single free-text
   cell. Stored in the DB so all viewers see the same overlay; the
   originals (titleAr/En, attendees, start/endTime) stay untouched and
   are restored on Unmerge.

Implementation notes
- DB: 3 nullable columns added to executive_meetings
  (merge_start_column, merge_end_column, merge_text). Applied via
  direct ALTER TABLE because pnpm --filter db run push is blocked by
  the pre-existing app_permissions duplicate (Task #148, surfaced as
  the new follow-up #156). Schema file kept in sync.
- API: meetingPatchSchema accepts an optional `merge` field
  (`null | {start,end,text}`) with start<=end ordering and server-side
  sanitizeRichText on the text. Reuses the existing PATCH route.
- Frontend: new MergeMenu component + canonical-order range resolution
  so merges still render when boundary columns are hidden, and
  gracefully degrade to unmerged rendering (without losing DB state)
  when column reordering makes the visible span non-contiguous.
  Unmerge stays available whenever a stored merge exists, even in
  degraded state, and existing mergeText is preserved on re-merge.
- i18n: en + ar keys under executiveMeetings.merge.*

Out of scope (deferred to follow-ups #154 / #155, per task brief)
- API + e2e test coverage for the merge endpoint and UI flows
- Realtime emission for executive-meetings mutations (no existing
  emit on this surface; covered by #155)

Validation
- TypeScript compiles cleanly across api-server + tx-os.
- API tests: 108/110 pass — the 2 failures are the pre-existing
  app_permissions-unique cases (Task #148, follow-up #156),
  unrelated to this change.
- Architect code review approved after addressing hidden-column
  fallback, non-contiguous reorder degradation, and
  Unmerge-availability edge cases.
2026-04-29 14:05:21 +00:00
Riyadh ec2797bba3 Add e2e tests for the receiver-side incoming-orders flow
Task #102: Adds Playwright UI coverage for the receiver side of the
service-orders flow, mirroring the existing API-level matrix in
artifacts/api-server/tests/service-orders.test.mjs.

What's added
- artifacts/tx-os/tests/order-receiver-flow.spec.mjs with two tests:
  1) "receiver claims, prepares, completes — requester sees live status
     updates": seeds a requester (role "user") and a receiver (role
     "order_receiver"), runs each in its own browser context, has the
     requester place an order via /services, then has the receiver claim
     it from /orders/incoming and walk it through Mark Preparing →
     Mark Completed. After every receiver action it asserts that the
     requester's /my-orders status pill ("Received", "Preparing",
     "Completed") updates without a manual refresh — exercising the
     order_updated socket.io push wired through use-notifications-socket.
  2) "receiver cancels after claiming — requester sees Cancelled pill":
     covers the receiver-side cancel path required by the task by
     claiming the order and then cancelling from /orders/incoming, and
     verifies the requester's card flips to the red "Cancelled" pill and
     loses the timeline step circles.

Test design notes
- English UI only; the language matrix is already covered for the
  requester side by order-place-cancel.spec.mjs.
- Each test creates a unique requester display name so that incoming
  cards can be uniquely located by service name + "From <display name>"
  even if other cards exist on the page.
- Status assertions use exact-text matching on the pill to avoid the
  "Received" / "Awaiting receiver" substring overlap.
- afterAll cleans up service_orders, notifications, user_roles and the
  seeded users (defensive: also deletes any orders referencing those
  users that weren't explicitly tracked).

Other
- The new spec is automatically picked up by the existing
  `pnpm --filter @workspace/tx-os test:e2e` run (testMatch in
  artifacts/tx-os/playwright.config.mjs already globs *.spec.mjs).
- Verified locally: full tx-os e2e suite (9 specs) passes.
2026-04-29 13:45:24 +00:00
riyadhafraa 2ffd63832a Add e2e tests for the receiver-side incoming-orders flow
Task #102: Adds Playwright UI coverage for the receiver side of the
service-orders flow, mirroring the existing API-level matrix in
artifacts/api-server/tests/service-orders.test.mjs.

What's added
- artifacts/tx-os/tests/order-receiver-flow.spec.mjs with two tests:
  1) "receiver claims, prepares, completes — requester sees live status
     updates": seeds a requester (role "user") and a receiver (role
     "order_receiver"), runs each in its own browser context, has the
     requester place an order via /services, then has the receiver claim
     it from /orders/incoming and walk it through Mark Preparing →
     Mark Completed. After every receiver action it asserts that the
     requester's /my-orders status pill ("Received", "Preparing",
     "Completed") updates without a manual refresh — exercising the
     order_updated socket.io push wired through use-notifications-socket.
  2) "receiver cancels after claiming — requester sees Cancelled pill":
     covers the receiver-side cancel path required by the task by
     claiming the order and then cancelling from /orders/incoming, and
     verifies the requester's card flips to the red "Cancelled" pill and
     loses the timeline step circles.

Test design notes
- English UI only; the language matrix is already covered for the
  requester side by order-place-cancel.spec.mjs.
- Each test creates a unique requester display name so that incoming
  cards can be uniquely located by service name + "From <display name>"
  even if other cards exist on the page.
- Status assertions use exact-text matching on the pill to avoid the
  "Received" / "Awaiting receiver" substring overlap.
- afterAll cleans up service_orders, notifications, user_roles and the
  seeded users (defensive: also deletes any orders referencing those
  users that weren't explicitly tracked).

Other
- The new spec is automatically picked up by the existing
  `pnpm --filter @workspace/tx-os test:e2e` run (testMatch in
  artifacts/tx-os/playwright.config.mjs already globs *.spec.mjs).
- Verified locally: full tx-os e2e suite (9 specs) passes.

Replit-Task-Id: bdbee3d1-20e0-4a40-b2b1-51ea16a15bb7
2026-04-29 13:45:24 +00:00
Riyadh 8ba1517d4b Transitioned from Plan to Build mode 2026-04-29 13:45:21 +00:00
riyadhafraa 1eb5ca11fb Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2c56ba37-edcd-4e0b-b6e1-4b185ddb982f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/n3Cg2XG
Replit-Helium-Checkpoint-Created: true
2026-04-29 13:45:21 +00:00
Riyadh 667033b127 Push role permission changes to signed-in users in real time
Original task (#101): When an admin saves the role editor, role holders
need their permission-driven UI (admin nav items, permission-gated
action buttons, etc.) to refresh without waiting for a page reload.
The server already emitted `apps_changed`, but that event is
semantically about visible apps, not the broader cached permission
state, and the client only invalidated the apps list and `/api/auth/me`.

Changes:
- artifacts/api-server/src/lib/realtime.ts
  - Extracted role-holder resolution (direct user_roles + indirect via
    group_roles -> user_groups) into a private helper.
  - Added `emitRolePermissionsChangedToHolders(roleId)` that emits a
    new `role_permissions_changed` socket event with `{ roleId }` to
    every holder of the role.
- artifacts/api-server/src/routes/roles.ts
  - After `PUT /api/roles/:id/permissions` succeeds, call the new
    emitter alongside the existing `emitAppsChangedToRoleHolders`.
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
  - Subscribed to `role_permissions_changed`. Invalidates
    `getMe`, `listRoles`, `listPermissions`, and—when the payload
    includes the role id—`getRolePermissions`, the role permission
    audit, and role usage queries so admin-only UI re-evaluates without
    a refresh.

Scope notes / deviations:
- The task spec called out `PUT /api/roles/:id/permissions` only, so
  the matching POST/DELETE single-permission endpoints still emit only
  `apps_changed`. Filed as follow-up #150 to keep them consistent.
- No new automated tests added (filed as follow-up #151). Pre-existing
  typecheck errors in artifacts/api-server/src/routes/executive-meetings.ts
  are unrelated and untouched.
- `replit.md` not updated; this is a behavioral refinement of an
  existing socket flow, not an architectural change.
2026-04-29 13:24:16 +00:00
riyadhafraa 10a48657c7 Push role permission changes to signed-in users in real time
Original task (#101): When an admin saves the role editor, role holders
need their permission-driven UI (admin nav items, permission-gated
action buttons, etc.) to refresh without waiting for a page reload.
The server already emitted `apps_changed`, but that event is
semantically about visible apps, not the broader cached permission
state, and the client only invalidated the apps list and `/api/auth/me`.

Changes:
- artifacts/api-server/src/lib/realtime.ts
  - Extracted role-holder resolution (direct user_roles + indirect via
    group_roles -> user_groups) into a private helper.
  - Added `emitRolePermissionsChangedToHolders(roleId)` that emits a
    new `role_permissions_changed` socket event with `{ roleId }` to
    every holder of the role.
- artifacts/api-server/src/routes/roles.ts
  - After `PUT /api/roles/:id/permissions` succeeds, call the new
    emitter alongside the existing `emitAppsChangedToRoleHolders`.
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
  - Subscribed to `role_permissions_changed`. Invalidates
    `getMe`, `listRoles`, `listPermissions`, and—when the payload
    includes the role id—`getRolePermissions`, the role permission
    audit, and role usage queries so admin-only UI re-evaluates without
    a refresh.

Scope notes / deviations:
- The task spec called out `PUT /api/roles/:id/permissions` only, so
  the matching POST/DELETE single-permission endpoints still emit only
  `apps_changed`. Filed as follow-up #150 to keep them consistent.
- No new automated tests added (filed as follow-up #151). Pre-existing
  typecheck errors in artifacts/api-server/src/routes/executive-meetings.ts
  are unrelated and untouched.
- `replit.md` not updated; this is a behavioral refinement of an
  existing socket flow, not an architectural change.

Replit-Task-Id: bd31a9a9-dbf1-497a-8505-96672f92bc79
2026-04-29 13:24:16 +00:00
riyadhafraa cdacb079e9 Git commit prior to merge 2026-04-29 13:24:16 +00:00
Riyadh d0095d9b77 Audit role permission changes (Task #100)
Record an audit trail every time a role's permissions change and surface
recent history inside the role edit dialog.

Schema (lib/db):
- New `role_permission_audit` table: id, roleId (FK roles), actorUserId
  (FK users, nullable on delete), previousPermissionIds int[],
  newPermissionIds int[], createdAt. Created via raw SQL because
  `drizzle push` currently fails on a pre-existing app_permissions
  duplicate (followup #148 tracks fixing this).

API (artifacts/api-server):
- PUT /api/roles/:id/permissions now wraps the permission delete/insert,
  the legacy audit_logs row, and the new role_permission_audit row in
  a *single* transaction so the permission set and its audit trail
  always commit (or roll back) together. Previous IDs are also read
  inside the transaction to avoid races.
- A role_permission_audit row is written on EVERY PUT call (per task
  spec), including no-op saves; the GET handler computes
  addedPermissionIds/removedPermissionIds so the History UI can
  distinguish meaningful changes from no-op saves.
- New GET /api/roles/:id/audit (admin-only, ?limit=1..50, default 10)
  returns recent entries newest-first with computed
  added/removedPermissionIds and joined actor info.
- New tests in tests/role-permission-audit.test.mjs cover write,
  every-call write (incl. no-op), GET ordering+diff+actor, no-op diff
  reporting, 404, and limit.

Drive-by fixes:
- Fixed pre-existing syntax corruption in tests/apps-open.test.mjs
  (stray `ccaPassa,` line from a prior bad merge that prevented the
  whole api-server test workflow from running).
- Made tests/roles-crud.test.mjs "rejects an invalid name" robust to
  parallel test execution by scoping the leak check to the bad name
  instead of relying on a global row count.

API spec / codegen (lib/api-spec, lib/api-client-react):
- Added `getRolePermissionAudit` operation and
  `RolePermissionAuditEntry` schema; ran orval codegen.

Frontend (artifacts/tx-os):
- New RolePermissionHistory component renders inside the role edit
  dialog (between permissions and Save/Cancel) using
  useGetRolePermissionAudit. Shows timestamp, actor, added/removed
  permission names (resolved via permissionsById), and total count.
- Save invalidates the audit query so the new entry appears immediately
  on the next open.
- Bilingual i18n strings (en + ar with full Arabic plural variants:
  zero/one/two/few/many/other) under admin.roles.history*.

Verification:
- tx-os typecheck passes.
- All 5 new audit tests + 13 existing roles tests pass.
- E2E (testing skill) verified the full flow: empty state on a fresh
  role, save adds a permission, reopened dialog shows the new entry
  with actor name in Arabic.

Docs:
- replit.md updated to list `role_permission_audit` in Database Tables.

Follow-ups proposed: #146 paginate/filter history, #147 audit other
admin permission changes, #148 fix drizzle push duplicate.
2026-04-29 13:16:02 +00:00
riyadhafraa a14b589006 Audit role permission changes (Task #100)
Record an audit trail every time a role's permissions change and surface
recent history inside the role edit dialog.

Schema (lib/db):
- New `role_permission_audit` table: id, roleId (FK roles), actorUserId
  (FK users, nullable on delete), previousPermissionIds int[],
  newPermissionIds int[], createdAt. Created via raw SQL because
  `drizzle push` currently fails on a pre-existing app_permissions
  duplicate (followup #148 tracks fixing this).

API (artifacts/api-server):
- PUT /api/roles/:id/permissions now wraps the permission delete/insert,
  the legacy audit_logs row, and the new role_permission_audit row in
  a *single* transaction so the permission set and its audit trail
  always commit (or roll back) together. Previous IDs are also read
  inside the transaction to avoid races.
- A role_permission_audit row is written on EVERY PUT call (per task
  spec), including no-op saves; the GET handler computes
  addedPermissionIds/removedPermissionIds so the History UI can
  distinguish meaningful changes from no-op saves.
- New GET /api/roles/:id/audit (admin-only, ?limit=1..50, default 10)
  returns recent entries newest-first with computed
  added/removedPermissionIds and joined actor info.
- New tests in tests/role-permission-audit.test.mjs cover write,
  every-call write (incl. no-op), GET ordering+diff+actor, no-op diff
  reporting, 404, and limit.

Drive-by fixes:
- Fixed pre-existing syntax corruption in tests/apps-open.test.mjs
  (stray `ccaPassa,` line from a prior bad merge that prevented the
  whole api-server test workflow from running).
- Made tests/roles-crud.test.mjs "rejects an invalid name" robust to
  parallel test execution by scoping the leak check to the bad name
  instead of relying on a global row count.

API spec / codegen (lib/api-spec, lib/api-client-react):
- Added `getRolePermissionAudit` operation and
  `RolePermissionAuditEntry` schema; ran orval codegen.

Frontend (artifacts/tx-os):
- New RolePermissionHistory component renders inside the role edit
  dialog (between permissions and Save/Cancel) using
  useGetRolePermissionAudit. Shows timestamp, actor, added/removed
  permission names (resolved via permissionsById), and total count.
- Save invalidates the audit query so the new entry appears immediately
  on the next open.
- Bilingual i18n strings (en + ar with full Arabic plural variants:
  zero/one/two/few/many/other) under admin.roles.history*.

Verification:
- tx-os typecheck passes.
- All 5 new audit tests + 13 existing roles tests pass.
- E2E (testing skill) verified the full flow: empty state on a fresh
  role, save adds a permission, reopened dialog shows the new entry
  with actor name in Arabic.

Docs:
- replit.md updated to list `role_permission_audit` in Database Tables.

Follow-ups proposed: #146 paginate/filter history, #147 audit other
admin permission changes, #148 fix drizzle push duplicate.

Replit-Task-Id: 9b8886a2-6e76-4072-b345-a772421fa161
2026-04-29 13:16:02 +00:00
Riyadh 1ad759cf48 Task #141: Make schedule row reordering work on iPad/touch devices
Original task: Fix iPad/touch drag-to-reorder of meeting rows in the
Executive Meetings daily schedule. Two root causes identified in the
spec:
  1. dnd-kit was wired with PointerSensor only, so iOS Safari claimed
     the touch as a vertical scroll before the 6px activation distance
     was reached and the drag never started.
  2. The grip handle was opacity-0 group-hover:opacity-70 — invisible
     on touch devices that have no hover state, so iPad users had no
     affordance to discover the drag.

Changes (all in artifacts/tx-os/src/pages/executive-meetings.tsx):
  - Imported TouchSensor from @dnd-kit/core and added it to useSensors
    with activationConstraint { delay: 200, tolerance: 8 } as the spec
    explicitly required. PointerSensor (distance: 6) and KeyboardSensor
    are unchanged.
  - Updated the row grip button className with arbitrary Tailwind
    media-query variants:
      [@media(hover:none)_and_(pointer:coarse)]:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:group-hover:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:p-1.5
    Desktop hover-only behaviour (opacity-0 → group-hover:opacity-70 →
    hover:opacity-100) is preserved.
  - Enlarged the GripVertical icon on touch (w-3.5/h-3.5 → w-4/h-4)
    for a friendlier tap target.
  - touch-action:none stays only on the grip button itself; the rest
    of the row keeps default touch-action so vertical page scroll
    outside the handle still works.

Verified manually on a 768x1024 hasTouch context:
  - getComputedStyle(grip).opacity = "0.4"
  - matchMedia('(hover:none) and (pointer:coarse)').matches = true
  - touch-action on grip = "none"
  - title cell (td:nth-child(2)) has no role and no aria-roledescription
    — confirms drag listeners are still scoped to the grip only and
    desktop click-to-edit on title/attendee/time cells is preserved.

Code review: PASS (architect).

Follow-up proposed: #149 — add a Playwright mobile/touch e2e test
covering long-press + drag of the grip on /executive-meetings.

Pre-existing test workflow failure (api-server tests + admin.tsx
codegen drift from earlier tasks) is unrelated and out of scope.
2026-04-29 13:01:41 +00:00
riyadhafraa 19b5dc788e Task #141: Make schedule row reordering work on iPad/touch devices
Original task: Fix iPad/touch drag-to-reorder of meeting rows in the
Executive Meetings daily schedule. Two root causes identified in the
spec:
  1. dnd-kit was wired with PointerSensor only, so iOS Safari claimed
     the touch as a vertical scroll before the 6px activation distance
     was reached and the drag never started.
  2. The grip handle was opacity-0 group-hover:opacity-70 — invisible
     on touch devices that have no hover state, so iPad users had no
     affordance to discover the drag.

Changes (all in artifacts/tx-os/src/pages/executive-meetings.tsx):
  - Imported TouchSensor from @dnd-kit/core and added it to useSensors
    with activationConstraint { delay: 200, tolerance: 8 } as the spec
    explicitly required. PointerSensor (distance: 6) and KeyboardSensor
    are unchanged.
  - Updated the row grip button className with arbitrary Tailwind
    media-query variants:
      [@media(hover:none)_and_(pointer:coarse)]:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:group-hover:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:p-1.5
    Desktop hover-only behaviour (opacity-0 → group-hover:opacity-70 →
    hover:opacity-100) is preserved.
  - Enlarged the GripVertical icon on touch (w-3.5/h-3.5 → w-4/h-4)
    for a friendlier tap target.
  - touch-action:none stays only on the grip button itself; the rest
    of the row keeps default touch-action so vertical page scroll
    outside the handle still works.

Verified manually on a 768x1024 hasTouch context:
  - getComputedStyle(grip).opacity = "0.4"
  - matchMedia('(hover:none) and (pointer:coarse)').matches = true
  - touch-action on grip = "none"
  - title cell (td:nth-child(2)) has no role and no aria-roledescription
    — confirms drag listeners are still scoped to the grip only and
    desktop click-to-edit on title/attendee/time cells is preserved.

Code review: PASS (architect).

Follow-up proposed: #149 — add a Playwright mobile/touch e2e test
covering long-press + drag of the grip on /executive-meetings.

Pre-existing test workflow failure (api-server tests + admin.tsx
codegen drift from earlier tasks) is unrelated and out of scope.
2026-04-29 13:01:41 +00:00
Riyadh d65f8d7fb8 Task #141: Make schedule row reordering work on iPad/touch devices
Original task: Fix iPad/touch drag-to-reorder of meeting rows in the
Executive Meetings daily schedule. Two root causes identified in the
spec:
  1. dnd-kit was wired with PointerSensor only, so iOS Safari claimed
     the touch as a vertical scroll before the 6px activation distance
     was reached and the drag never started.
  2. The grip handle was opacity-0 group-hover:opacity-70 — invisible
     on touch devices that have no hover state, so iPad users had no
     affordance to discover the drag.

Changes (all in artifacts/tx-os/src/pages/executive-meetings.tsx):
  - Imported TouchSensor from @dnd-kit/core and added it to useSensors
    with activationConstraint { delay: 250, tolerance: 5 } — matches
    the values already used in home.tsx so touch behaviour is
    consistent across the OS. PointerSensor (distance: 6) and
    KeyboardSensor are unchanged.
  - Updated the row grip button className with arbitrary Tailwind
    media-query variants:
      [@media(hover:none)_and_(pointer:coarse)]:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:group-hover:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:p-1.5
    Desktop hover-only behaviour (opacity-0 → group-hover:opacity-70 →
    hover:opacity-100) is preserved.
  - Enlarged the GripVertical icon on touch (w-3.5/h-3.5 → w-4/h-4)
    for a friendlier tap target.
  - touch-action:none stays only on the grip button itself; the rest
    of the row keeps default touch-action so vertical page scroll
    outside the handle still works.

Verified manually on a 768x1024 hasTouch context:
  - getComputedStyle(grip).opacity = "0.4"
  - matchMedia('(hover:none) and (pointer:coarse)').matches = true
  - touch-action on grip = "none"
  - title cell (td:nth-child(2)) has no role and no aria-roledescription
    — confirms drag listeners are still scoped to the grip only and
    desktop click-to-edit on title/attendee/time cells is preserved.

Code review: PASS (architect).

Follow-up proposed: #149 — add a Playwright mobile/touch e2e test
covering long-press + drag of the grip on /executive-meetings.

Pre-existing test workflow failure (api-server tests + admin.tsx
codegen drift from earlier tasks) is unrelated and out of scope.
2026-04-29 13:00:19 +00:00
riyadhafraa c372430a2c Task #141: Make schedule row reordering work on iPad/touch devices
Original task: Fix iPad/touch drag-to-reorder of meeting rows in the
Executive Meetings daily schedule. Two root causes identified in the
spec:
  1. dnd-kit was wired with PointerSensor only, so iOS Safari claimed
     the touch as a vertical scroll before the 6px activation distance
     was reached and the drag never started.
  2. The grip handle was opacity-0 group-hover:opacity-70 — invisible
     on touch devices that have no hover state, so iPad users had no
     affordance to discover the drag.

Changes (all in artifacts/tx-os/src/pages/executive-meetings.tsx):
  - Imported TouchSensor from @dnd-kit/core and added it to useSensors
    with activationConstraint { delay: 250, tolerance: 5 } — matches
    the values already used in home.tsx so touch behaviour is
    consistent across the OS. PointerSensor (distance: 6) and
    KeyboardSensor are unchanged.
  - Updated the row grip button className with arbitrary Tailwind
    media-query variants:
      [@media(hover:none)_and_(pointer:coarse)]:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:group-hover:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:p-1.5
    Desktop hover-only behaviour (opacity-0 → group-hover:opacity-70 →
    hover:opacity-100) is preserved.
  - Enlarged the GripVertical icon on touch (w-3.5/h-3.5 → w-4/h-4)
    for a friendlier tap target.
  - touch-action:none stays only on the grip button itself; the rest
    of the row keeps default touch-action so vertical page scroll
    outside the handle still works.

Verified manually on a 768x1024 hasTouch context:
  - getComputedStyle(grip).opacity = "0.4"
  - matchMedia('(hover:none) and (pointer:coarse)').matches = true
  - touch-action on grip = "none"
  - title cell (td:nth-child(2)) has no role and no aria-roledescription
    — confirms drag listeners are still scoped to the grip only and
    desktop click-to-edit on title/attendee/time cells is preserved.

Code review: PASS (architect).

Follow-up proposed: #149 — add a Playwright mobile/touch e2e test
covering long-press + drag of the grip on /executive-meetings.

Pre-existing test workflow failure (api-server tests + admin.tsx
codegen drift from earlier tasks) is unrelated and out of scope.
2026-04-29 13:00:19 +00:00
Riyadh 4379c7294c Show users/groups impact before removing role permissions (Task #99)
Adds a per-permission impact preview to the role editor so admins can see
who would lose each permission before saving — and a confirmation step
when the change would actually revoke access from at least one user.

API
- New endpoint POST /api/roles/:id/permissions/impact-preview
- Body: { permissionIds: number[] } (the proposed kept set)
- Response: { removed: [{ permissionId, permissionName, userCount,
  groupCount, groups: [{id,name}] }], totalAffectedUsers }
- A user is counted as "affected" only when no other role they hold
  (direct or via groups) still grants the removed permission.
- Implemented in artifacts/api-server/src/routes/roles.ts using two
  drizzle selectDistinct queries (direct + via groups) merged in JS.
  Switched away from a sql`${arr}::int[]` approach that failed because
  drizzle expands JS arrays as a parameter list, not as a single array
  parameter. Now uses inArray().

OpenAPI / client
- Added schemas RolePermissionsImpactBody, RolePermissionImpactGroup,
  RolePermissionImpactItem, RolePermissionsImpact in
  lib/api-spec/openapi.yaml.
- Regenerated lib/api-client-react/src/generated/* (new
  usePreviewRolePermissionsImpact hook).

UI
- artifacts/tx-os/src/pages/admin.tsx RolesPanel:
  - Debounced (350ms) on-demand fetch only when at least one permission
    has been unchecked relative to the saved state.
  - Inline amber summary panel listing each removed permission with
    user/group counts, plus a total-affected-users line.
  - Confirmation modal before save when totalAffectedUsers > 0.
- Added EN/AR translations (impactTitle, impactPerPermission,
  impactViaGroups, impactTotal, confirmRemoval*) using i18next
  _one/_other plural keys to match existing pluralized strings.

Tests
- New artifacts/api-server/tests/role-permissions-impact.test.mjs
  (6 tests, all green): empty removals, group-derived impact,
  "covered by another role" exclusion, 404 on unknown role, 400 on
  invalid body, 403 for non-admin.
- Existing roles-crud tests still pass.
- E2E flow verified end to end via the testing tool: amber summary
  appears, confirmation dialog gates the destructive save, and the
  resulting permission set persists.

Code-review follow-ups (applied in this same task)
- Tightened the OpenAPI 404 description for the new endpoint to clarify
  that unknown permission IDs in the body are tolerated (only a missing
  role yields 404). Regenerated the API client.
- Added a request-sequencing guard in the role editor so a stale
  in-flight impact-preview response cannot overwrite a newer selection.
- When the impact preview fails while removals exist, the inline panel
  now shows an explicit error message and the Save button is disabled
  until the user resolves it (e.g. by changing the selection so the
  preview can re-run successfully). EN/AR translations added under
  admin.roles.impactError.

Notes
- The pre-existing test workflow failure (ECONNREFUSED on 127.0.0.1:8080
  before the API server is ready) is unrelated and already tracked.
- replit.md not updated — change is endpoint-level and does not alter
  documented architecture.
- A modification to artifacts/tx-os/public/opengraph.jpg may show up in
  the diff; it is a build artifact regenerated by the vite dev server
  on restart and is not part of this change.
2026-04-29 12:45:19 +00:00
riyadhafraa 3aa6e81ed7 Show users/groups impact before removing role permissions (Task #99)
Adds a per-permission impact preview to the role editor so admins can see
who would lose each permission before saving — and a confirmation step
when the change would actually revoke access from at least one user.

API
- New endpoint POST /api/roles/:id/permissions/impact-preview
- Body: { permissionIds: number[] } (the proposed kept set)
- Response: { removed: [{ permissionId, permissionName, userCount,
  groupCount, groups: [{id,name}] }], totalAffectedUsers }
- A user is counted as "affected" only when no other role they hold
  (direct or via groups) still grants the removed permission.
- Implemented in artifacts/api-server/src/routes/roles.ts using two
  drizzle selectDistinct queries (direct + via groups) merged in JS.
  Switched away from a sql`${arr}::int[]` approach that failed because
  drizzle expands JS arrays as a parameter list, not as a single array
  parameter. Now uses inArray().

OpenAPI / client
- Added schemas RolePermissionsImpactBody, RolePermissionImpactGroup,
  RolePermissionImpactItem, RolePermissionsImpact in
  lib/api-spec/openapi.yaml.
- Regenerated lib/api-client-react/src/generated/* (new
  usePreviewRolePermissionsImpact hook).

UI
- artifacts/tx-os/src/pages/admin.tsx RolesPanel:
  - Debounced (350ms) on-demand fetch only when at least one permission
    has been unchecked relative to the saved state.
  - Inline amber summary panel listing each removed permission with
    user/group counts, plus a total-affected-users line.
  - Confirmation modal before save when totalAffectedUsers > 0.
- Added EN/AR translations (impactTitle, impactPerPermission,
  impactViaGroups, impactTotal, confirmRemoval*) using i18next
  _one/_other plural keys to match existing pluralized strings.

Tests
- New artifacts/api-server/tests/role-permissions-impact.test.mjs
  (6 tests, all green): empty removals, group-derived impact,
  "covered by another role" exclusion, 404 on unknown role, 400 on
  invalid body, 403 for non-admin.
- Existing roles-crud tests still pass.
- E2E flow verified end to end via the testing tool: amber summary
  appears, confirmation dialog gates the destructive save, and the
  resulting permission set persists.

Code-review follow-ups (applied in this same task)
- Tightened the OpenAPI 404 description for the new endpoint to clarify
  that unknown permission IDs in the body are tolerated (only a missing
  role yields 404). Regenerated the API client.
- Added a request-sequencing guard in the role editor so a stale
  in-flight impact-preview response cannot overwrite a newer selection.
- When the impact preview fails while removals exist, the inline panel
  now shows an explicit error message and the Save button is disabled
  until the user resolves it (e.g. by changing the selection so the
  preview can re-run successfully). EN/AR translations added under
  admin.roles.impactError.

Notes
- The pre-existing test workflow failure (ECONNREFUSED on 127.0.0.1:8080
  before the API server is ready) is unrelated and already tracked.
- replit.md not updated — change is endpoint-level and does not alter
  documented architecture.
- A modification to artifacts/tx-os/public/opengraph.jpg may show up in
  the diff; it is a build artifact regenerated by the vite dev server
  on restart and is not part of this change.

Replit-Task-Id: 30b7a2f7-7b60-429a-89d4-79280a81803f
2026-04-29 12:45:19 +00:00
Riyadh 791e0ddaf8 Improve tap target size for attendee elements on touch devices
Adjusted CSS for attendee elements to increase tap target size by adding padding and min-height, improving usability on touch devices like iPads.
2026-04-29 12:44:58 +00:00
riyadhafraa 3634769117 Improve tap target size for attendee elements on touch devices
Adjusted CSS for attendee elements to increase tap target size by adding padding and min-height, improving usability on touch devices like iPads.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 54d6551d-3f41-47b7-aad2-35c81e5d9b06
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/n3Cg2XG
Replit-Helium-Checkpoint-Created: true
2026-04-29 12:44:58 +00:00
Riyadh 48bf09a628 Task #140: 3 attendees-cell refinements in Executive Meetings schedule
(1) Clearing an attendee's name now removes the attendee row instead of
saving an empty record. saveAttendeeName detects empty stripped HTML
(tags + &nbsp; + whitespace) and PUTs the array with the row removed
and sortOrder repacked.

(2) Inline "+ Add attendee" button next to each attendee group, plus
"+ Virtual / + Internal / + External" chips for groups not yet present
in a split cell. Implemented as an optimistic ghost row — clicking +
sets a single global pendingAttendee state and renders a synthetic
EditableCell at the end of the matching group. Only when the user
types a non-empty name and blurs do we PUT the new attendee. Empty
blur or Escape just clears the pending state — no orphan rows reach
the API. The API stays strict (name.min(1)).

(3) Always-visible dashed underline on each attendee EditableCell
makes the multi-attendee wrapped layout discoverable as click targets;
hidden in edit mode (data-[editing=true]) and on print. The group
label ("— Virtual attendance — Webex —") is now pointer-events:none
so clicks pass through to whatever sits underneath instead of being
swallowed by the header text.

Implementation notes:
- EditableCell gained two new props (forceSaveOnBlur, onCancel) so the
  ghost row can commit/discard cleanly even when the editor is empty.
- pendingAttendee is a single global state (only one ghost open at a
  time across the whole page). All other rows hide their + buttons
  while a ghost is open to prevent the user from orphaning typing.
- Locale keys added in ar.json + en.json under
  executiveMeetings.schedule.{addAttendee, addVirtualAttendee, …,
  newAttendeeAria, editAttendeeAria}.
- Backend executive-meetings.ts unchanged — attendeeSchema still
  requires non-empty name.

Verified end-to-end:
- Add attendee, commit on blur, delete by clearing, cancel by Escape
  on empty.
- Cross-row gating: opening a ghost in row A hides "+" buttons in
  row B until the ghost is committed/cancelled.
- Webex/virtual header click no longer enters edit mode.

Pre-existing TypeScript errors in admin.tsx (codegen drift from #96)
and 3 failing api-server tests are unrelated and out of scope.

Follow-ups proposed: #144 (inline edit attendee titles),
#145 (move attendees between virtual/internal/external groups).
2026-04-29 12:42:45 +00:00
riyadhafraa b5de44485e Task #140: 3 attendees-cell refinements in Executive Meetings schedule
(1) Clearing an attendee's name now removes the attendee row instead of
saving an empty record. saveAttendeeName detects empty stripped HTML
(tags + &nbsp; + whitespace) and PUTs the array with the row removed
and sortOrder repacked.

(2) Inline "+ Add attendee" button next to each attendee group, plus
"+ Virtual / + Internal / + External" chips for groups not yet present
in a split cell. Implemented as an optimistic ghost row — clicking +
sets a single global pendingAttendee state and renders a synthetic
EditableCell at the end of the matching group. Only when the user
types a non-empty name and blurs do we PUT the new attendee. Empty
blur or Escape just clears the pending state — no orphan rows reach
the API. The API stays strict (name.min(1)).

(3) Always-visible dashed underline on each attendee EditableCell
makes the multi-attendee wrapped layout discoverable as click targets;
hidden in edit mode (data-[editing=true]) and on print. The group
label ("— Virtual attendance — Webex —") is now pointer-events:none
so clicks pass through to whatever sits underneath instead of being
swallowed by the header text.

Implementation notes:
- EditableCell gained two new props (forceSaveOnBlur, onCancel) so the
  ghost row can commit/discard cleanly even when the editor is empty.
- pendingAttendee is a single global state (only one ghost open at a
  time across the whole page). All other rows hide their + buttons
  while a ghost is open to prevent the user from orphaning typing.
- Locale keys added in ar.json + en.json under
  executiveMeetings.schedule.{addAttendee, addVirtualAttendee, …,
  newAttendeeAria, editAttendeeAria}.
- Backend executive-meetings.ts unchanged — attendeeSchema still
  requires non-empty name.

Verified end-to-end:
- Add attendee, commit on blur, delete by clearing, cancel by Escape
  on empty.
- Cross-row gating: opening a ghost in row A hides "+" buttons in
  row B until the ghost is committed/cancelled.
- Webex/virtual header click no longer enters edit mode.

Pre-existing TypeScript errors in admin.tsx (codegen drift from #96)
and 3 failing api-server tests are unrelated and out of scope.

Follow-ups proposed: #144 (inline edit attendee titles),
#145 (move attendees between virtual/internal/external groups).
2026-04-29 12:42:45 +00:00
riyadhafraa e5e96e7022 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: eab87fb0-38a8-43ae-95c6-49598fc45ab4
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/NvUGneQ
Replit-Helium-Checkpoint-Created: true
2026-04-29 12:22:40 +00:00
Riyadh 1b306ad16b Task #137: Polish Executive Meetings schedule table (5 refinements + post-review fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect reviews:
- Add row scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
  autoEditTitleId hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts.
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (start > end) raises a destructive toast with
  i18n key executiveMeetings.schedule.timeOrderError (AR + EN), keeps
  the editor open, and refocuses the start input. Equal start/end is
  valid per spec. Server errors also toasted.
- Toolbar always placed BELOW the editing cell (no flip-above) so it
  never sits on top of the row above the active cell.
- Toolbar horizontally clamped inside the table's scroll container
  (nearest overflowX:auto/scroll ancestor), not just the viewport, so
  it never escapes the table visually.
- Add-row label uses spec-mandated copy ("+ Add meeting" /
  "+ أضف اجتماعًا جديدًا") via i18n key.
- Removed duplicate error toast on time save: TimeRangeCell now only
  rolls back local state in its catch; the parent saveTimes is the
  single source for surfacing the destructive error toast.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + post-review fixes pass.
  * Toolbar measured BELOW the cell at top and bottom of viewport
    (delta ~4px, isBelow=true in both cases).
  * Toolbar measured INSIDE the table scroll container's left/right.
  * Equal start/end (14:00–14:00) saved without validation toast.
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 12:04:43 +00:00
riyadhafraa eaba29095e Task #137: Polish Executive Meetings schedule table (5 refinements + post-review fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect reviews:
- Add row scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
  autoEditTitleId hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts.
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (start > end) raises a destructive toast with
  i18n key executiveMeetings.schedule.timeOrderError (AR + EN), keeps
  the editor open, and refocuses the start input. Equal start/end is
  valid per spec. Server errors also toasted.
- Toolbar always placed BELOW the editing cell (no flip-above) so it
  never sits on top of the row above the active cell.
- Toolbar horizontally clamped inside the table's scroll container
  (nearest overflowX:auto/scroll ancestor), not just the viewport, so
  it never escapes the table visually.
- Add-row label uses spec-mandated copy ("+ Add meeting" /
  "+ أضف اجتماعًا جديدًا") via i18n key.
- Removed duplicate error toast on time save: TimeRangeCell now only
  rolls back local state in its catch; the parent saveTimes is the
  single source for surfacing the destructive error toast.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + post-review fixes pass.
  * Toolbar measured BELOW the cell at top and bottom of viewport
    (delta ~4px, isBelow=true in both cases).
  * Toolbar measured INSIDE the table scroll container's left/right.
  * Equal start/end (14:00–14:00) saved without validation toast.
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 12:04:43 +00:00
Riyadh 85f2bb9190 Task #137: Polish Executive Meetings schedule table (5 refinements + post-review fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect reviews:
- Add row scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
  autoEditTitleId hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts.
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (start > end) raises a destructive toast with
  i18n key executiveMeetings.schedule.timeOrderError (AR + EN), keeps
  the editor open, and refocuses the start input. Equal start/end is
  valid per spec. Server errors also toasted.
- Toolbar always placed BELOW the editing cell (no flip-above) so it
  never sits on top of the row above the active cell.
- Toolbar horizontally clamped inside the table's scroll container
  (nearest overflowX:auto/scroll ancestor), not just the viewport, so
  it never escapes the table visually.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + post-review fixes pass.
  * Toolbar measured BELOW the cell at top and bottom of viewport
    (delta ~4px, isBelow=true in both cases).
  * Toolbar measured INSIDE the table scroll container's left/right.
  * Equal start/end (14:00–14:00) saved without validation toast.
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 11:59:00 +00:00
riyadhafraa 1ba05c11d0 Task #137: Polish Executive Meetings schedule table (5 refinements + post-review fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect reviews:
- Add row scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
  autoEditTitleId hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts.
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (start > end) raises a destructive toast with
  i18n key executiveMeetings.schedule.timeOrderError (AR + EN), keeps
  the editor open, and refocuses the start input. Equal start/end is
  valid per spec. Server errors also toasted.
- Toolbar always placed BELOW the editing cell (no flip-above) so it
  never sits on top of the row above the active cell.
- Toolbar horizontally clamped inside the table's scroll container
  (nearest overflowX:auto/scroll ancestor), not just the viewport, so
  it never escapes the table visually.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + post-review fixes pass.
  * Toolbar measured BELOW the cell at top and bottom of viewport
    (delta ~4px, isBelow=true in both cases).
  * Toolbar measured INSIDE the table scroll container's left/right.
  * Equal start/end (14:00–14:00) saved without validation toast.
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 11:59:00 +00:00
Riyadh 86bd8aa66e Task #137: Polish Executive Meetings schedule table (5 refinements + 4 follow-up fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect review:
- Add row now scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (end <= start, including equal times) now raises a
  destructive toast with i18n key executiveMeetings.schedule.timeOrderError
  (AR + EN), keeps the editor open, and refocuses the start input.
  Server errors also toasted.
- autoEditTitleId state hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts (e.g., column hidden).
- Toolbar prefers placement BELOW the editing cell (only flips above
  when no room below in viewport), so it never covers the row above.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + 4 fixes pass; toolbar prefer-below
  measured cell.bottom=276.84 / toolbar.top=280.84 on a top-of-viewport
  row (correctly flips above only at viewport bottom).
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 11:50:41 +00:00
riyadhafraa df28f4f0d4 Task #137: Polish Executive Meetings schedule table (5 refinements + 4 follow-up fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect review:
- Add row now scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (end <= start, including equal times) now raises a
  destructive toast with i18n key executiveMeetings.schedule.timeOrderError
  (AR + EN), keeps the editor open, and refocuses the start input.
  Server errors also toasted.
- autoEditTitleId state hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts (e.g., column hidden).
- Toolbar prefers placement BELOW the editing cell (only flips above
  when no room below in viewport), so it never covers the row above.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + 4 fixes pass; toolbar prefer-below
  measured cell.bottom=276.84 / toolbar.top=280.84 on a top-of-viewport
  row (correctly flips above only at viewport bottom).
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 11:50:41 +00:00
Riyadh 444293bba8 Task #137: Five refinements to the Executive Meetings schedule table
Original ask (Arabic): make the time fit on one line, let users edit
the time inline, make inline-editing of attendee names reliable
(including for empty names), keep the formatting toolbar from
covering the table, and add an inline "Add row" button at the bottom
of the table.

Changes
-------
1) Time on a single line + inline-editable
   - artifacts/tx-os/src/pages/executive-meetings.tsx
     * New `TimeRangeCell` component renders "HH:MM – HH:MM" on a
       single line with `whitespace-nowrap`.
     * Click / Enter / Space enters edit mode with two side-by-side
       `<input type="time">` controls (start / end).
     * Saves on blur (debounced) or Enter; Esc cancels and restores
       the previous values.
     * Client-side guard: refuses to save when start > end (server
       also returns a 400 with a validation error).
     * `saveTimes` callback wires to PATCH /api/executive-meetings/:id
       with `{startTime, endTime}` (null clears).
     * Time column default width bumped 120 → 200 to keep one line at
       the default zoom.

2) Inline edit attendee names (incl. empty names)
   - artifacts/tx-os/src/pages/executive-meetings.tsx (AttendeeFlow)
     * EditableCell for attendee names now uses `placeholder="…"`,
       `dir="auto"`, and a min-width / padding so an empty name is
       still a visible click target and respects RTL/LTR.

3) Toolbar that doesn't cover the table
   - artifacts/tx-os/src/components/editable-cell.tsx
     * `FormattingToolbar` now renders through `createPortal` to
       `document.body` with `position: fixed`.
     * Position is recomputed via `getBoundingClientRect` with
       above/below flip and horizontal viewport clamp; tracked by
       `ResizeObserver` plus scroll/resize listeners.
     * The shared toolbar ref is consulted in `onFocusOut` and a new
       document `pointerdown` listener so clicks inside the toolbar
       no longer close the editor.

4) Inline "Add row" at the bottom of the table
   - artifacts/tx-os/src/pages/executive-meetings.tsx
     * New `<tr data-testid="em-add-row-tr">` rendered after the
       sortable rows, gated on `canMutate && !isLoading`, with
       `print:hidden`.
     * `createRow` callback POSTs to /api/executive-meetings with
       bilingual default titles ("اجتماع جديد" / "New meeting") for
       the currently selected `meetingDate`.

5) i18n
   - artifacts/tx-os/src/locales/{ar,en}.json:
     * Added `executiveMeetings.schedule.{addRow,
       newMeetingDefaultAr, newMeetingDefaultEn, timeStart, timeEnd}`
       in both locales.

Verification
------------
- Manual API: POST creates row (201), PATCH valid times (200), PATCH
  invalid (start > end) returns the validation 400, PATCH null
  clears times, DELETE returns 204.
- Browser E2E (Playwright): all five refinements verified end-to-end
  on /executive-meetings. The agent's own cleanup step initially
  reported a stale row but the row was actually deleted server-side
  (verified by 404 on direct GET).
- Architect review: PASS, no blockers; constraints (Tasks #122/#125,
  no TeaBoy references, bilingual i18n) all satisfied.

Note
----
- artifacts/tx-os/public/opengraph.jpg was inadvertently touched
  earlier in the session; restored to its pre-build state in this
  commit. No real change to the asset.
2026-04-29 11:32:11 +00:00
riyadhafraa a045445759 Task #137: Five refinements to the Executive Meetings schedule table
Original ask (Arabic): make the time fit on one line, let users edit
the time inline, make inline-editing of attendee names reliable
(including for empty names), keep the formatting toolbar from
covering the table, and add an inline "Add row" button at the bottom
of the table.

Changes
-------
1) Time on a single line + inline-editable
   - artifacts/tx-os/src/pages/executive-meetings.tsx
     * New `TimeRangeCell` component renders "HH:MM – HH:MM" on a
       single line with `whitespace-nowrap`.
     * Click / Enter / Space enters edit mode with two side-by-side
       `<input type="time">` controls (start / end).
     * Saves on blur (debounced) or Enter; Esc cancels and restores
       the previous values.
     * Client-side guard: refuses to save when start > end (server
       also returns a 400 with a validation error).
     * `saveTimes` callback wires to PATCH /api/executive-meetings/:id
       with `{startTime, endTime}` (null clears).
     * Time column default width bumped 120 → 200 to keep one line at
       the default zoom.

2) Inline edit attendee names (incl. empty names)
   - artifacts/tx-os/src/pages/executive-meetings.tsx (AttendeeFlow)
     * EditableCell for attendee names now uses `placeholder="…"`,
       `dir="auto"`, and a min-width / padding so an empty name is
       still a visible click target and respects RTL/LTR.

3) Toolbar that doesn't cover the table
   - artifacts/tx-os/src/components/editable-cell.tsx
     * `FormattingToolbar` now renders through `createPortal` to
       `document.body` with `position: fixed`.
     * Position is recomputed via `getBoundingClientRect` with
       above/below flip and horizontal viewport clamp; tracked by
       `ResizeObserver` plus scroll/resize listeners.
     * The shared toolbar ref is consulted in `onFocusOut` and a new
       document `pointerdown` listener so clicks inside the toolbar
       no longer close the editor.

4) Inline "Add row" at the bottom of the table
   - artifacts/tx-os/src/pages/executive-meetings.tsx
     * New `<tr data-testid="em-add-row-tr">` rendered after the
       sortable rows, gated on `canMutate && !isLoading`, with
       `print:hidden`.
     * `createRow` callback POSTs to /api/executive-meetings with
       bilingual default titles ("اجتماع جديد" / "New meeting") for
       the currently selected `meetingDate`.

5) i18n
   - artifacts/tx-os/src/locales/{ar,en}.json:
     * Added `executiveMeetings.schedule.{addRow,
       newMeetingDefaultAr, newMeetingDefaultEn, timeStart, timeEnd}`
       in both locales.

Verification
------------
- Manual API: POST creates row (201), PATCH valid times (200), PATCH
  invalid (start > end) returns the validation 400, PATCH null
  clears times, DELETE returns 204.
- Browser E2E (Playwright): all five refinements verified end-to-end
  on /executive-meetings. The agent's own cleanup step initially
  reported a stale row but the row was actually deleted server-side
  (verified by 404 on direct GET).
- Architect review: PASS, no blockers; constraints (Tasks #122/#125,
  no TeaBoy references, bilingual i18n) all satisfied.

Note
----
- artifacts/tx-os/public/opengraph.jpg was inadvertently touched
  earlier in the session; restored to its pre-build state in this
  commit. No real change to the asset.
2026-04-29 11:32:11 +00:00
Riyadh 3dbd43c8cd Task #98: Warn admins before renaming a role used in code references
- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
  guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
  renamedFrom, and renamedTo when the role name changes (in addition to
  existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
  regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
  updated intentionally to preserve `export * from './manual'` in
  api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
  on open and shows an amber warning banner with from/to text plus a
  usage line (users + groups counts) whenever the trimmed name input
  differs from the original. Warning is suppressed for system or
  protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
  renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
  session — it is unrelated to this task.

Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.

Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).
2026-04-29 10:57:44 +00:00
riyadhafraa 59c7338d63 Task #98: Warn admins before renaming a role used in code references
- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
  guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
  renamedFrom, and renamedTo when the role name changes (in addition to
  existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
  regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
  updated intentionally to preserve `export * from './manual'` in
  api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
  on open and shows an amber warning banner with from/to text plus a
  usage line (users + groups counts) whenever the trimmed name input
  differs from the original. Warning is suppressed for system or
  protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
  renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
  session — it is unrelated to this task.

Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.

Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).

Replit-Task-Id: c1ee048d-50c6-4439-a430-ee0b7ec09959
2026-04-29 10:57:44 +00:00
Riyadh 4cfa236fa3 Git commit prior to merge 2026-04-29 10:57:44 +00:00
riyadhafraa 0d58a856d6 Git commit prior to merge 2026-04-29 10:57:44 +00:00
riyadhafraa 768d6eab4c Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c18e1a47-f2d8-412d-86fc-05ad16b0f23a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/9GxtwFd
Replit-Helium-Checkpoint-Created: true
2026-04-29 10:46:32 +00:00
Riyadh 342b59de4b Git commit prior to merge 2026-04-29 10:32:54 +00:00
riyadhafraa 896f690ac0 Git commit prior to merge 2026-04-29 10:32:54 +00:00
Riyadh 1d47573947 Task #125: Restore meetings table on mobile, allow pinch-zoom
Reverts the phone-only stacked-card layout introduced in Task #119
back to a single tidy table for every viewport, and re-enables
pinch-zoom on the whole app.

Changes:
- artifacts/tx-os/index.html: viewport meta no longer pins
  maximum-scale=1, so iOS/Android pinch gestures work everywhere
  (now: width=device-width, initial-scale=1).
- artifacts/tx-os/src/pages/executive-meetings.tsx:
  * Removed the md:hidden cards branch (the entire em-schedule-cards
    block) so the table is the only schedule view at every width.
  * Dropped the `hidden md:block print:block` gating on the table
    container; it now renders at all viewports inside the existing
    overflow-x-auto wrapper, so a too-wide table scrolls horizontally
    inside its own container instead of breaking the page layout.
  * Deleted the now-orphaned MeetingCard component and its only
    consumer of the inline-only RowColorPickerInline variant.
  * Deleted RowColorPickerInline (no remaining consumer); the hover
    RowColorPicker is still used by table rows.

Preserved:
- Desktop (≥xl) fixed-width + resizable column behavior is unchanged
  (table-fixed at xl, resize handles still mouse-only).
- Print fixed-layout behavior (`print:table-fixed`) and RTL support
  are unchanged.
- Localized labels for the customizer/highlight popover are unchanged.

Verification:
- pnpm --filter @workspace/tx-os exec tsc --noEmit shows no new
  errors in executive-meetings.tsx (pre-existing admin.tsx errors
  from the just-merged Task #96 codegen drift are unrelated).
- The table renders at 390 / 768 / 1280 px widths; horizontal scroll
  appears only when the table is wider than the container.

Out of scope (per task spec):
- Print/PDF page (executive-meetings-print.tsx) — Task #120.
- API, schema, columns, or RBAC — none changed.
2026-04-29 09:06:25 +00:00
riyadhafraa ed52b845f1 Task #125: Restore meetings table on mobile, allow pinch-zoom
Reverts the phone-only stacked-card layout introduced in Task #119
back to a single tidy table for every viewport, and re-enables
pinch-zoom on the whole app.

Changes:
- artifacts/tx-os/index.html: viewport meta no longer pins
  maximum-scale=1, so iOS/Android pinch gestures work everywhere
  (now: width=device-width, initial-scale=1).
- artifacts/tx-os/src/pages/executive-meetings.tsx:
  * Removed the md:hidden cards branch (the entire em-schedule-cards
    block) so the table is the only schedule view at every width.
  * Dropped the `hidden md:block print:block` gating on the table
    container; it now renders at all viewports inside the existing
    overflow-x-auto wrapper, so a too-wide table scrolls horizontally
    inside its own container instead of breaking the page layout.
  * Deleted the now-orphaned MeetingCard component and its only
    consumer of the inline-only RowColorPickerInline variant.
  * Deleted RowColorPickerInline (no remaining consumer); the hover
    RowColorPicker is still used by table rows.

Preserved:
- Desktop (≥xl) fixed-width + resizable column behavior is unchanged
  (table-fixed at xl, resize handles still mouse-only).
- Print fixed-layout behavior (`print:table-fixed`) and RTL support
  are unchanged.
- Localized labels for the customizer/highlight popover are unchanged.

Verification:
- pnpm --filter @workspace/tx-os exec tsc --noEmit shows no new
  errors in executive-meetings.tsx (pre-existing admin.tsx errors
  from the just-merged Task #96 codegen drift are unrelated).
- The table renders at 390 / 768 / 1280 px widths; horizontal scroll
  appears only when the table is wider than the container.

Out of scope (per task spec):
- Print/PDF page (executive-meetings-print.tsx) — Task #120.
- API, schema, columns, or RBAC — none changed.
2026-04-29 09:06:25 +00:00
Riyadh 8af9a17af5 Task #96: Show dependency counts in admin app/service/user lists
Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.

Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
  (groupCount, restrictionCount, openCount), Service (orderCount),
  and UserProfile (noteCount, orderCount, conversationCount,
  messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
  batch-loads groupCount/restrictionCount/openCount via grouped
  COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
  batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
  loads noteCount/orderCount/conversationCount/messageCount from
  notes, service_orders, conversations.created_by, and
  messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
  delete buttons pre-populate their conflict-state from the row's
  count fields so the DeletionWarningDialog shows the warning on
  the first click. The lazy 409 fallback still works as a safety
  net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.

Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
  with 6 tests verifying each list endpoint exposes the new count
  fields with the expected non-zero values when dependents exist
  AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
  (9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
  service delete icon ONCE opens the confirmation modal with the
  dependency warning ("orders", count >= 1) immediately.

Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
  schemas (not list-only sub-schemas) so the same shape is returned
  from list and detail endpoints. Code review flagged this as
  acceptable but broader than strictly necessary; left as is to
  keep the API consistent.

Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
  two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
  existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
  isn't already up — needs a readiness check.
2026-04-29 08:48:29 +00:00
riyadhafraa c0cf2115dd Task #96: Show dependency counts in admin app/service/user lists
Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.

Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
  (groupCount, restrictionCount, openCount), Service (orderCount),
  and UserProfile (noteCount, orderCount, conversationCount,
  messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
  batch-loads groupCount/restrictionCount/openCount via grouped
  COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
  batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
  loads noteCount/orderCount/conversationCount/messageCount from
  notes, service_orders, conversations.created_by, and
  messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
  delete buttons pre-populate their conflict-state from the row's
  count fields so the DeletionWarningDialog shows the warning on
  the first click. The lazy 409 fallback still works as a safety
  net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.

Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
  with 6 tests verifying each list endpoint exposes the new count
  fields with the expected non-zero values when dependents exist
  AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
  (9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
  service delete icon ONCE opens the confirmation modal with the
  dependency warning ("orders", count >= 1) immediately.

Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
  schemas (not list-only sub-schemas) so the same shape is returned
  from list and detail endpoints. Code review flagged this as
  acceptable but broader than strictly necessary; left as is to
  keep the API consistent.

Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
  two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
  existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
  isn't already up — needs a readiness check.

Replit-Task-Id: bd14fe73-9961-431b-ab5a-ab70f116e8c7
2026-04-29 08:48:29 +00:00