Commit Graph

122 Commits

Author SHA1 Message Date
riyadhafraa 6876a83bbb Audit log: filter by actor (#197)
Admins can now filter the audit log by who acted, not just by what was
acted on.

User-facing changes
- Replaced the flat actor <select> with an autocomplete combobox
  (Popover + cmdk Command) that searches by username AND display name,
  in English or Arabic.
- Added an `actorId` URL hash param that survives full reload — picks
  up alongside the existing target filter on initial mount and is
  cleared when the admin navigates to a different section.
- Each audit row's actor avatar + name is now clickable, mirroring the
  existing target chip pivot, so admins can jump from "this row" to
  "everything by this person" in one click. Rows with a null actor
  (system / deleted user) render the same avatar/name without the
  click affordance, since the API can only filter by a concrete id.
- Active actor pill renders in sky (target pill remains emerald) and
  has a clear button.
- CSV export already accepted `actorUserId` on the backend; the
  frontend export call now forwards the filter and the OpenAPI
  description has been updated to document it.

Files
- artifacts/tx-os/src/pages/admin.tsx
  - New AuditActorPicker component, hash helpers
    (parseAuditHashActor / syncAuditHashActor), pivotToActor /
    clearActorFilter handlers, sky-colored active pill, clickable
    actor in AuditLogRow.
- artifacts/api-server/tests/audit-logs-actor-filter.test.mjs (new)
  - 6 tests covering: filter narrows results, excludes other actors
    on the same target_type, combines with target filter, invalid
    /zero/negative actorUserId → 400, CSV export honors filter.
- artifacts/tx-os/src/locales/{en,ar}.json
  - actorSearch / actorEmpty / actorWithName / actorWithId /
    clearActorFilter / actorPivotAria.
- lib/api-spec/openapi.yaml
  - Audit export endpoint description now mentions
    targetType / targetId / actorUserId.

Verification
- pnpm --filter @workspace/tx-os run typecheck → clean.
- All 6 new actor-filter tests + all 7 existing target-filter tests
  pass against a live API server.
- E2E run via runTest() succeeded: opened the picker, selected an
  actor, verified the pill + reload-safe hash, cleared, pivoted via
  a row click, and confirmed CSV export honored the filter.

Notes / drift
- URL key is `actorId` (per task spec), but the React state and API
  param remain `actorUserId` to match the existing backend.
- The broader `test` workflow has pre-existing failures in
  executive-meetings.test.mjs and service-orders.test.mjs unrelated
  to this change (admin 401 / HTML-404 fallback). Captured as
  follow-up #291.

Replit-Task-Id: 0f02b232-eda3-46db-8235-98ecce2ebdb7
2026-05-01 15:26:36 +00:00
riyadhafraa ab5ec2e2e2 Add shared row highlighting to executive meeting scheduler
Implement shared row highlighting for executive meetings by adding a `rowColor` field to the database schema and API, and migrating existing per-device colors to the new shared field.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 273accfc-a301-41b9-bd20-c121cb4e79c7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/g7BgHDL
Replit-Helium-Checkpoint-Created: true
2026-05-01 15:24:59 +00:00
riyadhafraa 2a005646f3 Task #196: Add inline "Recent activity" sections to admin detail panels
- Added shared `RecentActivityForTarget` component in `artifacts/tx-os/src/pages/admin.tsx` that lists the 10 most recent audit entries for a given (targetType, targetId), reusing `formatAuditSummary` for consistent wording with the audit log section, plus actor + timestamp metadata. Renders loading / empty / list states with stable data-testids.

- Added `openAuditLogForTarget(targetType, targetId)` helper inside `AdminPage`. Writes the URL hash with `section=audit-log&targetType=<t>&targetId=<n>` BEFORE calling `setSection("audit-log")` so the existing section-sync effect (which would otherwise drop section-scoped params on a section change) preserves the deep-link parameters. Also closes any open editor modals.

- Wired the component into all five entity detail panels:
  * App edit modal
  * Service edit modal
  * `UserGroupsEditor` (user edit)
  * `GroupDetailEditor` (history tab)
  * Role edit dialog
  GroupsPanel and RolesPanel previously took no props; both now accept and forward an `onOpenAuditLogForTarget` prop. `OpenAuditLogForTarget` type is shared.

- Added i18n keys `admin.audit.recent.{title,loading,empty,viewAll,viewAllAria}` in both `en.json` and `ar.json`.

- Verified end-to-end via Playwright runTest: deep-link hash, modal closure, and audit-log filter pre-population all work for app/service/group/role/user panels.

No backend changes required (the existing `targetType`/`targetId` filter on `GET /api/audit` was already supported).

Replit-Task-Id: c03451f9-a6bb-4dd4-b082-f34ce3b6001d
2026-05-01 14:58:32 +00:00
riyadhafraa 9607a02047 Task #194: Show deleted user's full name in admin Audit Log
Original task
-------------
The admin Audit Log row for a user.delete entry showed
"Target: user #1234 · @ahmed" — i.e. the @username — even though
admins recognise people by name. Surface displayNameEn / displayNameAr
in that line and fall back to @username only when no display name
exists.

Backend
-------
artifacts/api-server/src/routes/users.ts
The user.delete handler already persists `displayNameEn` and
`displayNameAr` in audit metadata (added in commit eb7d15c). No
code change was needed on the API side. Verified the metadata
shape via the existing audit-log-coverage test.

Frontend
--------
artifacts/tx-os/src/pages/admin.tsx
- Renamed `forceDeletedEntityName` to `deletedEntityName` and
  updated the doc-comment.
- Relaxed its gate so it returns a name not only for force-delete
  entries but also for plain `user.delete` entries. Other delete
  actions (app.delete, group.delete) still gate on force-delete
  to avoid changing behaviour outside this task's scope.
- Updated the single call-site to use the new function name.

Effect: a non-force user.delete row now renders a "Target: user
#1234 · Ahmed Al-Saleh" line beneath the summary, with locale-aware
fallback (Ar -> En -> @username).

Verification
------------
- pnpm api-spec codegen + tx-os tsc --noEmit: clean.
- artifacts/api-server `node --test` for audit-log-coverage,
  audit-logs-actor-filter, audit-logs-forced-only-filter,
  audit-logs-target-filter: 43 / 43 tests pass (CSV export tests
  included).
- e2e test via runTest: created a user with displayName
  "Ahmed Test User" / "أحمد مستخدم", deleted without force,
  opened /#section=audit-log, and confirmed the new row's
  summary plus the secondary "Target: user #19630 · أحمد مستخدم"
  line render correctly (admin's Arabic locale was honoured).

Replit-Task-Id: fc3fc8f9-082a-49e4-b223-17baee11d268
2026-05-01 14:00:39 +00:00
riyadhafraa b54761b166 Task #192: Show row color + merge range previews in row-actions kebab menu
Original task: The Executive Meetings row-actions kebab menu has three sub-views
(delete, color, merge). Users couldn't tell at a glance which colour or merge
range a row already had — they had to drill into the sub-view first. Add subtle
inline indicators to the main menu so the current state is visible without
extra clicks.

Implementation:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  • RowActionsMenu now renders an inline circular swatch next to the "Row
    color" item, reflecting the row's saved colour. "default" shows a hatched
    pattern matching the swatch picker; other keys show the saved tint.
    Decorative (aria-hidden) since the swatch sub-view is the actual control.
    Carries data-testid="em-row-color-indicator-{id}" plus a data-color-key
    attribute for stable, language-agnostic tests.
  • Renders a "Merged: <cols>" badge next to the "Merge cells" item only when
    the row has a stored merge. Column names come from the canonical merge
    order (number, meeting, attendees, time) and are localized via the
    existing executiveMeetings.col.{id} keys.
  • New mergeColumnLabels prop is computed in MeetingRow against
    CANONICAL_MERGE_ORDER (not visibleColumns) so the badge still describes
    the full saved range when a boundary column is hidden. Threaded into
    both call sites — the first-visible-cell anchor and the merged-cell
    anchor.
  • Widened the local t prop type to (k, opts?) so the badge can use the
    {{cols}} interpolation template.

- artifacts/tx-os/src/locales/en.json + ar.json
  • Added executiveMeetings.merge.activeBadge ("Merged: {{cols}}" /
    "مدموج: {{cols}}").

Tests:
- Added artifacts/tx-os/tests/executive-meetings-row-actions-previews.spec.mjs
  with two specs:
    1. Row-color indicator updates from "default" → "red" → "default" via the
       sub-view picker, and the dot's computed background tracks the saved
       colour (#fee2e2 → rgb(254,226,226)).
    2. Plain rows render no merge badge; merged rows render the badge with
       text "<MeetingColLabel> + <AttendeesColLabel>". Column labels are
       resolved from the live header DOM so the assertion works in both
       English and Arabic (admin's preferredLanguage overrides the
       tx-lang=en init seed after login).
  Both pass; existing executive-meetings-row-actions-menu.spec.mjs and
  executive-meetings-merge.spec.mjs continue to pass (5/5 regression).

Notable subtleties:
- A first attempt at the merge spec hit a flake where Escape-then-force-click
  on the next row's kebab landed on the previous popover's Delete item
  (which was still mounted), triggering a stray confirm dialog. Added an
  explicit waitFor on the previous popover's unmount before opening the
  next one.

Follow-up proposed: #276 (next_steps) — show the same cues directly on the
row itself, not only inside the kebab popover.

Replit-Task-Id: 7ff92dcd-f4bd-421d-93c8-4d7d3dbe0077
2026-05-01 13:44:21 +00:00
riyadhafraa f0888b6b82 Task #188: Add browser test for reorder rollback when the API rejects a row drag
Added a Playwright e2e scenario in
`artifacts/tx-os/tests/executive-meetings-schedule-features.spec.mjs`
that exercises the optimistic-update + onError rollback path in
`reorderRows()` (artifacts/tx-os/src/pages/executive-meetings.tsx ~L1484).

What the test does:
- Seeds two meetings (A, B) on a unique future date via direct DB inserts.
- Logs in as admin, navigates to /executive-meetings, jumps to that date,
  enables edit mode.
- Captures the original DOM order [A, B].
- Installs a `page.route("**/api/executive-meetings/reorder")` override
  that responds with 500 + `{ error: "Simulated reorder failure" }`. The
  `error` field (not `message`) is intentional so apiJson() throws that
  exact string into the destructive toast description.
- Drags row B above row A using the same mechanic as the success-path
  reorder test (warm-up move past dnd-kit's 6px PointerSensor activation
  distance, then a stepped move targeting just above the row's vertical
  center).
- Waits for the intercepted 500, then asserts the toast description
  ("Simulated reorder failure") and a localized title (en or ar) is
  visible — proving the user-facing error surfaced.
- Polls the DOM to confirm the row order rolled back to [A, B].
- Defense-in-depth: queries the DB to confirm daily_number / start_time /
  end_time for both rows are unchanged, so a future regression that
  somehow bypasses the route override can't hide.
- Cleans up: page.unroute, and the existing afterAll deletes the seeded
  rows.

Test seeding:
- Uses uniqueFutureDate(6); offsets 1..5 are already claimed by other
  tests in this file, and the file-level afterAll cleanup means two
  tests sharing date + daily_number would collide on the
  UNIQUE (meeting_date, daily_number) index.

Verified by running:
  npx playwright test --grep "rolls back to the original order"
and the prior success-path drag test together — both pass.

No production code changes; this is a pure test addition.

Replit-Task-Id: 02cfe898-1db8-40e9-ba6b-cd5df9f0a3f4
2026-05-01 12:53:59 +00:00
riyadhafraa f729026ce0 Task #273: 5-minute pre-meeting alert for Executive Meetings
Floating, draggable alert that appears on every Tx OS page when an
Executive Meeting is within five minutes of starting.

Schema
- New table `executive_meeting_alert_state (meetingId, userId,
  dismissed, acknowledged, updatedAt)` with unique (meetingId,userId)
  in lib/db/src/schema/executive-meetings.ts. Apply via
  `pnpm --filter @workspace/db run push-force` per environment.

API (artifacts/api-server/src/routes/executive-meetings.ts)
- GET  /executive-meetings/alert-state?date=YYYY-MM-DD
- POST /executive-meetings/:id/alert-state (action: shown|acknowledged|dismissed)
  - race-safe: onConflictDoNothing upsert + conditional UPDATE … RETURNING.
- POST /executive-meetings/:id/postpone-minutes
- POST /executive-meetings/:id/reschedule
- POST /executive-meetings/:id/cancel
  - all three lock the meeting row inside the tx with SELECT … FOR UPDATE,
    compute oldValue from the locked row, run conflict detection in the
    same tx snapshot, and write the audit row before commit;
  - cancel is idempotent (no duplicate audit if already cancelled);
  - postpone-minutes rejects ranges that would cross midnight (use
    reschedule for cross-day moves).
- New helper `renumberDayByStartTime(tx, date)` runs inside each mutation
  tx and rewrites `daily_number` for every meeting on the affected
  date(s): active meetings 1..N by start_time, cancelled meetings
  pushed to the tail. Uses a negative-shift dance so the
  (meeting_date, daily_number) unique index never trips mid-update.
  Reschedule renumbers both the old and new date when the day moves.
- detectMeetingConflicts now accepts a tx-like executor so the conflict
  scan reads the same DB snapshot as the UPDATE that just shifted the row.

Frontend
- New component artifacts/tx-os/src/components/executive-meetings/
  upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
  AuthProvider. Draggable with localStorage position persistence,
  RTL-aware, polls every 30 s, shows start–end window, postpone-by-
  minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
  cancel-meeting flow that requires an explicit confirm step before
  the destructive call fires.
- Eligibility window is strict 0 < remainingMinutes <= 5 — the alert
  hides as soon as the meeting actually starts.
- Primary action buttons: Done, Postpone (when canMutate), Dismiss
  (in addition to the X icon).
- artifacts/tx-os/src/pages/executive-meetings.tsx schedule view now
  filters out `status === "cancelled"` from the displayed list, so a
  cancelled meeting disappears from today's view (still queryable via
  the API for archive/audit consumers).

i18n
- New `executiveMeetings.alert.*` keys in both en.json and ar.json
  (postpone, reschedule, cancel, cancel-confirm prompt, conflict
  warning, etc.).

Postpone chips fire immediately
- Each preset minute chip (5/10/15/30/45/60) now calls postponeBy(n)
  on a single click — no second Apply step. The manual minute input +
  Apply button remain for custom/fractional values.

Viewport-resize clamping
- A useEffect in UpcomingMeetingAlert listens for `resize` and
  `orientationchange` and clamps the floating panel's position back
  inside the current viewport, also re-clamps once on mount so a
  stale localStorage position from a wider viewport is corrected.

Tests
- artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs —
  9 Playwright scenarios, all green:
    1. Done acknowledges the alert
    2. Postpone-10 shifts both start and end and clears the alert
    3. Cancel-with-confirm marks the meeting cancelled
    4. Dismiss (X) writes a dismissed audit row
    5. Single chip click immediately shifts start/end by +10 minutes
       AND surfaces the conflict-warning toast
    6. Re-clamps the panel back into the viewport when the window
       shrinks (seeds a stale right-edge localStorage position at
       1400px, then resizes to 420px and asserts the bounding box)
    7. Reschedule to a different day clears today's alert
    8. Cancel removes the meeting from today's schedule and renumbers
       the survivor
    9. Arabic locale renders the RTL alert with Arabic title

Copy
- "Dismiss alert" / "تجاهل التنبيه" wording on the dismiss control
  (and the toast that follows) to match the product spec.

Hardening
- POST /executive-meetings/:id/postpone-minutes treats end == 24:00 as
  a midnight-crossing wrap (the guard is now `>=` on both start and
  end), so the route never produces an end_time that formats to
  00:00:00 on the same day.

Docs
- replit.md updated with the new table, routes, and migration step.

Pre-existing tsc errors at lines 546, 662, and 2107 of
executive-meetings.ts are unrelated to this task.
2026-05-01 12:46:03 +00:00
riyadhafraa ad2f7615fb Task #273: 5-minute pre-meeting alert for Executive Meetings
Floating, draggable alert that appears on every Tx OS page when an
Executive Meeting is within five minutes of starting.

Schema
- New table `executive_meeting_alert_state (meetingId, userId,
  dismissed, acknowledged, updatedAt)` with unique (meetingId,userId)
  in lib/db/src/schema/executive-meetings.ts. Apply via
  `pnpm --filter @workspace/db run push-force` per environment.

API (artifacts/api-server/src/routes/executive-meetings.ts)
- GET  /executive-meetings/alert-state?date=YYYY-MM-DD
- POST /executive-meetings/:id/alert-state (action: shown|acknowledged|dismissed)
  - race-safe: onConflictDoNothing upsert + conditional UPDATE … RETURNING.
- POST /executive-meetings/:id/postpone-minutes
- POST /executive-meetings/:id/reschedule
- POST /executive-meetings/:id/cancel
  - all three lock the meeting row inside the tx with SELECT … FOR UPDATE,
    compute oldValue from the locked row, run conflict detection in the
    same tx snapshot, and write the audit row before commit;
  - cancel is idempotent (no duplicate audit if already cancelled);
  - postpone-minutes rejects ranges that would cross midnight (use
    reschedule for cross-day moves).
- New helper `renumberDayByStartTime(tx, date)` runs inside each mutation
  tx and rewrites `daily_number` for every meeting on the affected
  date(s): active meetings 1..N by start_time, cancelled meetings
  pushed to the tail. Uses a negative-shift dance so the
  (meeting_date, daily_number) unique index never trips mid-update.
  Reschedule renumbers both the old and new date when the day moves.
- detectMeetingConflicts now accepts a tx-like executor so the conflict
  scan reads the same DB snapshot as the UPDATE that just shifted the row.

Frontend
- New component artifacts/tx-os/src/components/executive-meetings/
  upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
  AuthProvider. Draggable with localStorage position persistence,
  RTL-aware, polls every 30 s, shows start–end window, postpone-by-
  minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
  cancel-meeting flow that requires an explicit confirm step before
  the destructive call fires.
- Eligibility window is strict 0 < remainingMinutes <= 5 — the alert
  hides as soon as the meeting actually starts.
- Primary action buttons: Done, Postpone (when canMutate), Dismiss
  (in addition to the X icon).
- artifacts/tx-os/src/pages/executive-meetings.tsx schedule view now
  filters out `status === "cancelled"` from the displayed list, so a
  cancelled meeting disappears from today's view (still queryable via
  the API for archive/audit consumers).

i18n
- New `executiveMeetings.alert.*` keys in both en.json and ar.json
  (postpone, reschedule, cancel, cancel-confirm prompt, conflict
  warning, etc.).

Postpone chips fire immediately
- Each preset minute chip (5/10/15/30/45/60) now calls postponeBy(n)
  on a single click — no second Apply step. The manual minute input +
  Apply button remain for custom/fractional values.

Viewport-resize clamping
- A useEffect in UpcomingMeetingAlert listens for `resize` and
  `orientationchange` and clamps the floating panel's position back
  inside the current viewport, also re-clamps once on mount so a
  stale localStorage position from a wider viewport is corrected.

Tests
- artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs —
  9 Playwright scenarios, all green:
    1. Done acknowledges the alert
    2. Postpone-10 shifts both start and end and clears the alert
    3. Cancel-with-confirm marks the meeting cancelled
    4. Dismiss (X) writes a dismissed audit row
    5. Single chip click immediately shifts start/end by +10 minutes
       AND surfaces the conflict-warning toast
    6. Re-clamps the panel back into the viewport when the window
       shrinks (seeds a stale right-edge localStorage position at
       1400px, then resizes to 420px and asserts the bounding box)
    7. Reschedule to a different day clears today's alert
    8. Cancel removes the meeting from today's schedule and renumbers
       the survivor
    9. Arabic locale renders the RTL alert with Arabic title

Copy
- "Dismiss alert" / "تجاهل التنبيه" wording on the dismiss control
  (and the toast that follows) to match the product spec.

Docs
- replit.md updated with the new table, routes, and migration step.

Pre-existing tsc errors at lines 546, 662, and 2107 of
executive-meetings.ts are unrelated to this task.
2026-05-01 12:26:31 +00:00
riyadhafraa a72c00fe19 Task #273: 5-minute pre-meeting alert for Executive Meetings
- New `executive_meeting_alert_state` table (per-user, per-meeting) tracking
  `dismissed`/`acknowledged`. Migration via `pnpm --filter @workspace/db
  run push-force`.
- New API routes in artifacts/api-server/src/routes/executive-meetings.ts:
  GET /alert-state, POST /:id/alert-state, POST /:id/postpone-minutes,
  POST /:id/reschedule, POST /:id/cancel. All three mutation routes
  acquire a row lock with SELECT ... FOR UPDATE inside the transaction,
  compute oldValue from the locked snapshot, run conflict detection in
  the same tx, and write the audit row before commit. Cancel is
  idempotent. Postpone-minutes rejects ranges that would cross midnight
  (use reschedule for cross-day moves).
- Alert-state route uses race-safe onConflictDoNothing upsert + a
  conditional UPDATE ... RETURNING so transition audits never duplicate.
- New i18n keys `executiveMeetings.alert.*` in en.json + ar.json,
  including the cancel-confirm prompt.
- New component artifacts/tx-os/src/components/executive-meetings/
  upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
  AuthProvider. Draggable with localStorage position persistence,
  RTL-aware, polls every 30s, shows start–end window, postpone-by-
  minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
  Cancel-meeting flow that requires an explicit confirm step before
  the destructive call fires (gated on confirmCancel state).
- Playwright spec executive-meetings-upcoming-alert.spec.mjs with
  6 scenarios: appear+Done, postpone-10 shifts times, cancel-with-
  confirm, dismiss audit, postpone-chip+conflict-warning toast,
  AR/RTL render. All 6 pass.
- replit.md updated with the new table, routes, and migration step.

Pre-existing tsc errors at lines 489, 605, and 2039 of
executive-meetings.ts are not touched by this task.
2026-05-01 11:56:14 +00:00
riyadhafraa a29f7a25c6 Add browser test for drag-to-reorder schedule columns (task #187)
Adds a Playwright scenario to
artifacts/tx-os/tests/executive-meetings-schedule-features.spec.mjs that:
- Logs in as admin, seeds one meeting on a far-future date, and lands
  on the schedule for that date.
- Hops to the Settings tab to confirm the column customizer panel
  (em-customize-columns-panel) still surfaces the feature, then
  bounces back to the schedule.
- Enters edit mode (required for SortableHeader to register dnd-kit
  attributes/listeners) and asserts the default header order
  (number, meeting, attendees, time) via th[data-testid^=em-col-header-].
- Drags the "attendees" header above the "meeting" header using a
  warm-up move past the 6px PointerSensor activation distance and a
  stepped pointer move whose end point lands slightly LEFT of the
  target's horizontal center (horizontalListSortingStrategy needs the
  drop point on the leading side to insert before the target).
- Polls the rendered headers for the new order
  (number, attendees, meeting, time), then reads
  em-schedule-cols-v1 from localStorage to confirm persistence.
- Reloads the page and re-asserts the new order to prove it restores
  from localStorage rather than reverting to DEFAULT_COLUMNS.

Deviation from the task wording: the task description says the test
should "open the customize-columns popover" and "drag one column chip
above another". The customize panel is no longer a popover — it was
moved into the Settings tab in #265 — and it never had draggable
chips; column reordering is wired to the SortableHeader cells inside
the schedule's floating thead. The new test honors the spirit of the
task by visiting the Settings panel for sanity, then performing the
actual reorder gesture on the table headers (the only mechanism the
codebase exposes).

Validation: the new test passes in isolation and as part of the
schedule-features suite. One unrelated existing test
("custom highlight color paints the current meeting's box-shadow
ring") is currently flaky/failing on its own without my changes;
left untouched as it is outside this task's scope.

Replit-Task-Id: 06c14e68-096b-407d-86b9-bd5a45674aee
2026-05-01 11:18:40 +00:00
riyadhafraa ad84a8abc6 #267: Freeze top bar, schedule heading, and table column header on scroll
Top bar and schedule-heading row already used `position: sticky` with
dynamic offsets published as `--em-header-h` / `--em-heading-h` via
ResizeObserver. The table column header is the new piece.

Approach: a floating sticky overlay (`em-sticky-thead`) rendered as a
DOM-sibling above the horizontally-scrollable wrapper, scroll-synced
in JS and column-width-measured from the first valid tbody row via
ResizeObserver. The original `<thead>` is hidden in screen mode but
kept (no testids, no Sortable) with `print:table-header-group` so
column labels still appear at the top of every printed page.

This replaces the rejected first attempt that relied on
`position:sticky` inside `overflow-x-auto`, which only worked at xl+
viewports. The floating overlay sticks reliably at mobile (414px),
tablet (~900px), desktop, and in RTL — all four covered by an
expanded sticky-header e2e spec.

Drag-reorder, resize handles, and the bulk-select tri-state checkbox
now live exclusively in the floating thead, eliminating the duplicate-
testid concern from having two interactive headers in the DOM.

Tests: 4/4 sticky-header, 5/5 bulk-actions, 6/6 edit-toggle, 7/7
keyboard editing, 1/1 touch reorder. The one failing schedule-features
case (custom highlight color) reproduces on the pre-change baseline
and is pre-existing flake unrelated to this work.

Files:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/tests/executive-meetings-sticky-header.spec.mjs
2026-05-01 09:54:18 +00:00
riyadhafraa aecc5f5eed Restore opengraph image to its previous state
Revert changes to artifacts/tx-os/public/opengraph.jpg to resolve unintended modifications.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8b4b2f5f-b778-466d-967b-d07190e7cccb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/zG2WcPi
Replit-Helium-Checkpoint-Created: true
2026-05-01 08:54:46 +00:00
riyadhafraa 310baa41ab #265: unify Executive Meetings header into single Settings tab
Header
- Removed the standalone Font Settings nav button and the bilingual
  language toggle. The header now exposes only Export PDF.
- Renamed the SECTIONS key `fontSettings` → `settings` (Settings icon
  retained) and updated visibility checks + render switch.

Settings tab
- New SettingsSection wraps the existing FontSettingsSection plus a
  new ColumnsCustomizerPanel (extracted from the old popover body) so
  column visibility / current-meeting highlight live with the rest of
  user prefs.
- Removed the old ColumnsCustomizer popover trigger from the schedule
  toolbar; the inline panel inside Settings is the single entry point.

State lifting
- columns/setColumns and highlightPrefs/setHighlightPrefs lifted from
  ScheduleSection up to ExecutiveMeetingsPage so both tabs read/write
  the same state. Storage keys (COLS_STORAGE_KEY, HIGHLIGHT_STORAGE_KEY)
  unchanged so existing user prefs continue to load.
- Per code-reviewer follow-up: moved the columns localStorage write
  effect up to the page as well so edits made in Settings persist even
  when ScheduleSection is unmounted.

Locales
- ar.json + en.json: renamed nav.fontSettings → nav.settings
  ("الإعدادات" / "Settings"); removed the now-unused
  executiveMeetings.fontSettings header label.
- Removed unused Languages and Type lucide imports.

Tests
- Updated executive-meetings-bulk-actions.spec.mjs and
  executive-meetings-schedule-features.spec.mjs to navigate via
  em-nav-settings instead of the removed em-customize-columns-trigger.
- All 226 sequential api tests pass; both updated playwright specs
  pass.
2026-05-01 08:52:33 +00:00
riyadhafraa 21c935064d #262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections — UI, locales, realtime
invalidations, backend routes, role lists, capability flags, schema
tables, notify lib, and tests.

Backend (artifacts/api-server)
- routes/executive-meetings.ts: deleted /requests* + /tasks* handler
  block, REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES,
  canSubmitRequest / canViewTasks / canViewAllTasks from /me, retired
  table imports, and dead schemas (detailsByType, requestPayloadSchemas,
  request*Schema, taskCreateSchema, taskPatchSchema, dueAtSchema,
  dateOnly, timeHm). canApprove kept (still used by FontSettings).
- lib/executive-meeting-notify.ts: EXECUTIVE_MEETING_NOTIFICATION_TYPES
  collapsed to ['meeting_created'].

Frontend (artifacts/tx-os)
- pages/executive-meetings.tsx: deleted RequestsSection /
  ApprovalsSection / TasksSection / RequestListRow, pruned SECTIONS,
  MeCapabilities / MeRoles types, isSectionVisible cases, icon imports.
- hooks/use-notifications-socket.ts: dropped the two retired query
  invalidations.
- locales/{ar,en}.json: removed nav.{requests,approvals,tasks},
  executiveMeetings.{requests,approvals,tasks} subtrees, and the 6
  retired notification.type entries.

Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables + relations + types
  for requests/tasks removed.
- artifacts/api-server/scripts/cleanup-em-requests-tasks.sql:
  idempotent BEGIN/COMMIT — deletes orphan prefs / notifications /
  audit rows, then DROP TABLE … CASCADE for both retired tables.
  Applied to dev DB and `db push` re-synced.

Tests
- executive-meetings.test.mjs: deleted 9 retired blocks + 2 covered
  prefs duplicates, rewrote /me capability test to assert flags absent,
  rewrote DELETE-wipe test to use meeting_created via POST
  /api/executive-meetings, removed /requests + /tasks router.param
  entries.
- executive-meetings-notifications.test.mjs: deleted 7 blocks
  (request_*, task_*, cross-event-mute), updated before/after
  cleanup to skip dropped tables, kept setPref/clearPref helpers
  (still used by surviving meeting_created opt-out tests).

Drift / pre-existing
- 3 test failures observed under the parallel `node --test` workflow
  (meeting_created fan-out count, pref opt-out daily-number conflict,
  service-orders JSON-vs-HTML) are pre-existing parallel-file
  pollution between executive-meetings.test.mjs and
  executive-meetings-notifications.test.mjs. Verified by running
  `node --test --test-concurrency=1 'tests/**/*.test.mjs'` →
  226/226 pass. Out of scope for #262.
- Pre-existing tsc warnings at routes/executive-meetings.ts L509/625
  (boolean/number on isHighlighted) and L1594 (font-settings scope
  query) untouched.
2026-05-01 08:18:29 +00:00
riyadhafraa c0e55752a3 Task #183: clickable dependency counts on admin Apps/Services/Users
Turned the inline dependency-count badges on each admin row into
focusable, keyboard-accessible buttons that open a drill-in modal
listing the actual rows behind the count.

Backend (artifacts/api-server/src/routes):
  - apps.ts: GET /admin/apps/:id/dependents/{groups,restrictions,opens}
  - services.ts: GET /admin/services/:id/dependents/orders
  - users.ts: GET /admin/users/:id/dependents/{notes,orders,conversations,messages}
  All paginated (limit default 50, max 200; offset) returning
  {items, totalCount, limit, offset, nextOffset}. Restrictions joins
  the role names that include each required permission.

OpenAPI:
  - Added 8 path operations + 16 schemas (Item + Page) and re-ran
    `pnpm --filter @workspace/api-spec run codegen`.

Frontend (artifacts/tx-os/src/pages/admin.tsx):
  - New DependencyDrillIn component reuses DrillInShell (Escape +
    backdrop close, RTL/LTR safe) and the existing LoadMoreSection
    pagination pattern from AppOpensDrillIn.
  - Each count part in Apps, Services, and Users panels is now a
    <button> with a unique data-testid (e.g. app-counts-groups-213,
    user-counts-messages-5) and an aria-label that reads
    "View {count}".
  - AdminPage owns dependencyTarget state for Apps/Services counts;
    UsersPanel owns its own (it already encapsulates user list).

Translations:
  - Added admin.dependents.* (titles, subtitles, empty/error/load
    labels, conversation/order/message helper strings, order status
    enum) to en.json and ar.json.

Verification: - tx-os typecheck clean; api-server has only the pre-existing
    executive-meetings.ts errors (untouched).
  - e2e tested via runTest: login as admin, opened Apps panel,
    drilled into groups + opens (Load more grew 50→100), drilled into
    Tea orders, drilled into user 5 conversations and messages,
    switched to Arabic/RTL and re-opened the Groups drill-in to
    confirm Arabic rendering with no raw i18n keys.
Replit-Task-Id: fe96a05b-325f-4e1f-901b-3a2235fb24b5
2026-05-01 07:32:11 +00:00
riyadhafraa eefcf20403 Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven
narrow-then-defer pattern from #242:

- #195 — Plain DELETE /api/services/:id now writes a `service.delete`
  audit row carrying nameEn + nameAr (force-with-deps still uses the
  dedicated `service.force_delete`). Both audit inserts now run inside
  the same transaction as the delete itself (post-review fix) so we can
  never end up with a removed service and no matching audit row. Added
  matching `service.delete` formatter case + EN/AR i18n keys, and
  surfaced nameAr on the existing `service.force_delete` summary.
- #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export.
  openapi.yaml updated, codegen regenerated, server filter wired through
  parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI
  got an actor dropdown wired into params + export URL + reset, and a
  new audit-logs-actor-filter API test (4 cases) covers list narrowing,
  exclusion, invalid input, and CSV export.
- #178 — Formatter unit tests for user.delete (id-only, EN/AR display
  name resolution, force flag, force + name) and the new service.delete
  (id-only, EN/AR), 11 new cases (33/33 pass).

Skipped #194 — already implemented; users.ts DELETE persists displayName
fields and audit-summary already renders user.deleteWithName/forceDeleteWithName.

Deferred via follow-ups (no duplicate of existing #182/#183/#184):
- F1: #196 recent-activity endpoint + 5 admin panels
- F2: #205+#206+#208 permission history CSV/name resolution/timeline
- F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs

Also reverts an unrelated stray binary change to
artifacts/tx-os/public/opengraph.jpg that got rolled into the prior
auto-commit — restored to its previous content.

Validation: tx-os typecheck clean; pre-existing executive-meetings.ts
errors not regressed; all targeted server tests pass (delete-force-warnings 10,
audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new
actor-filter 4, broader audit/services sweep 40); e2e test verified actor
dropdown rendering, filter behavior, readable Arabic service.delete summary,
and CSV export honoring the filter.
2026-05-01 06:57:50 +00:00
riyadhafraa b3ad701ba6 Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven
narrow-then-defer pattern from #242:

- #195 — Plain DELETE /api/services/:id now writes a `service.delete`
  audit row carrying nameEn + nameAr (force-with-deps still uses the
  dedicated `service.force_delete`). Added matching `service.delete`
  formatter case + EN/AR i18n keys, and surfaced nameAr on the existing
  `service.force_delete` summary.
- #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export.
  openapi.yaml updated, codegen regenerated, server filter wired through
  parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI
  got an actor dropdown wired into params + export URL + reset, and a
  new audit-logs-actor-filter API test (4 cases) covers list narrowing,
  exclusion, invalid input, and CSV export.
- #178 — Formatter unit tests for user.delete (id-only, EN/AR display
  name resolution, force flag, force + name) and the new service.delete
  (id-only, EN/AR), 11 new cases (33/33 pass).

Skipped #194 — already implemented; users.ts DELETE persists displayName
fields and audit-summary already renders user.deleteWithName/forceDeleteWithName.

Deferred via follow-ups (no duplicate of existing #182/#183/#184):
- F1: #196 recent-activity endpoint + 5 admin panels
- F2: #205+#206+#208 permission history CSV/name resolution/timeline
- F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs

Validation: tx-os typecheck clean; pre-existing executive-meetings.ts
errors not regressed; all targeted server tests pass (delete-force-warnings 10,
audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new
actor-filter 4, broader audit/services sweep 40); e2e test verified actor
dropdown rendering, filter behavior, readable Arabic service.delete summary,
and CSV export honoring the filter.
2026-05-01 06:54:26 +00:00
riyadhafraa 56524cf0de Friendly summaries for app.permission.add/remove audit entries
Task #174: The new `app.permission.add` and `app.permission.remove`
audit log actions emitted by `artifacts/api-server/src/routes/apps.ts`
were falling through `formatAuditSummary`'s default branch, so the
admin audit log only displayed the raw action string instead of a
human-readable sentence like the existing `role.permission.*` entries.

Changes:
- artifacts/tx-os/src/pages/admin.tsx: added `app.permission.add` and
  `app.permission.remove` cases to `formatAuditSummary`. They reuse
  the existing `appName(meta, lang, targetId)` helper (which already
  handles the nameEn/nameAr/slug fallback) and mirror the existing
  role.permission.* permission-name fallback (permissionName ->
  #permissionId -> "?").
- artifacts/tx-os/src/locales/en.json: added
  `admin.audit.summary.app.permissionAdd` /
  `admin.audit.summary.app.permissionRemove`, matching the wording of
  the role.* variants ("Added permission '{{permission}}' to app
  '{{name}}'", "Removed permission '{{permission}}' from app
  '{{name}}'").
- artifacts/tx-os/src/locales/ar.json: added the Arabic equivalents
  ("تمت إضافة الصلاحية '...' إلى التطبيق '...'", "تمت إزالة الصلاحية
  '...' من التطبيق '...'").

Verification:
- e2e tested via runTest: created a new app, added/removed a
  permission via the API to emit both audit entries, opened the
  admin audit log, and confirmed both rows render the friendly
  English summary; switched the UI to Arabic and confirmed the same
  rows render the Arabic summary (no raw "app.permission.add" /
  "app.permission.remove" strings shown).
- JSON files validated; no new TypeScript errors introduced near the
  edited lines (the pre-existing 36 codegen-related errors in
  admin.tsx are unrelated).

No deviations from the task scope.

Replit-Task-Id: eb2f3eea-089a-4afc-803c-2580cbf1cfc9
2026-04-30 20:27:15 +00:00
riyadhafraa fb2d75ecd7 Task #164: Per-user notification preferences for Executive Meetings
Lets each user choose whether to receive in-app and/or email notifications
for each executive-meeting event type (meeting_created, request_submitted,
request_approved/rejected/needs_edit, task_assigned, task_completed).
Defaults to "everything on" when no preference row exists, preserving the
prior fan-out behavior for users who never visit the new UI.

Schema:
- New executive_meeting_notification_prefs table (user_id FK CASCADE,
  notification_type varchar(64), in_app bool default true, email bool
  default true, plus a unique index on (user_id, notification_type)).
- Pushed to dev DB via `pnpm --filter @workspace/db push`.

Backend:
- Exported EXECUTIVE_MEETING_NOTIFICATION_TYPES (canonical list) +
  filterRecipientsByNotificationPref(ids, type, channel) helper that
  returns only recipients whose row says the channel is on (default-on
  semantics for missing rows).
- recordExecutiveMeetingNotifications now filters recipients by
  channel="inApp" before inserting; sendExecutiveMeetingEmail filters
  by channel="email" before SMTP delivery.
- New endpoints under /executive-meetings/notification-prefs:
  GET → { types, prefs } merged with defaults.
  PUT → upserts each supplied (type, channel) pair via
  onConflictDoUpdate inside a transaction.

Frontend:
- New NotificationPrefsCard at the top of the Notifications section in
  artifacts/tx-os/src/pages/executive-meetings.tsx. Renders a Switch per
  (event type × channel) with batched save, dirty-state tracking, reset
  button, and useToast feedback.
- Translation keys for the card added to en.json and ar.json under
  executiveMeetings.notificationsPage.prefs.

Tests:
- 5 new tests in artifacts/api-server/tests/executive-meetings.test.mjs:
  GET defaults, PUT roundtrip + upsert, 400 on unknown type, in-app
  fan-out filtering (muted approver gets no row, control approver still
  does), and channel-independence (muting only the email channel leaves
  in-app delivery intact while persisting email=false in the DB row that
  sendExecutiveMeetingEmail's filter reads).
- All 36 executive-meetings tests pass. Full suite shows only one
  pre-existing flaky test elsewhere (groups-crud count assertion),
  unrelated to these changes.
- Added e2e UI test that logs in as admin, toggles a preference, saves,
  refreshes, and confirms persistence.
- After-hook cleans up new prefs rows for created users.

Follow-ups proposed: #236 (one-click reset to defaults), #237 (admin
view/override of any user's prefs).

Replit-Task-Id: 284ce15d-40d7-447e-90ca-090b44d8227b
2026-04-30 18:56:05 +00:00
riyadhafraa 60a18e1c7c Task #162: Let admins pre-set required permissions while creating an app
The "Required permissions" section was previously edit-only because
`POST /api/apps/:id/permissions` needs an app id, leaving a brief
window where a freshly created app was visible to everyone before
the admin could re-open the dialog and gate it. The Add app dialog
now lets the admin pick required permissions up front and the new
app + its `app_permissions` rows are written in a single transaction.

Changes:
- `lib/api-spec/openapi.yaml`: extended `CreateAppBody` with an optional
  `permissionIds: integer[]` field. Ran `pnpm --filter @workspace/api-spec
  run codegen` so `lib/api-zod` and `lib/api-client-react` reflect it.
- `artifacts/api-server/src/routes/apps.ts`: `POST /apps` now de-dupes
  and pre-validates `permissionIds`, returns 404 if any id is unknown
  (without creating the app), and inside one transaction inserts the
  app, the `app_permissions` rows (with `.onConflictDoNothing()` against
  the composite primary key), and a single `permission_audit` row
  (`previousIds: []`, `newIds: requestedIds`). After the transaction it
  also writes one `app.permission.add` audit_logs entry per inserted
  permission so the admin log mirrors the post-create flow.
- `artifacts/tx-os/src/pages/admin.tsx`: added `permissionIds: number[]`
  to `AppForm`, a new `NewAppPermissionsPicker` component (rendered only
  in create mode — edit mode keeps the existing `AppPermissionsEditor`
  with its impact preview) that lets admins add/remove permissions
  locally before submit, and wired `handleSaveApp` to forward the
  selected ids when creating. Existing edit path strips the field so
  the update payload remains unchanged.
- `replit.md`: documented the new picker and POST /api/apps behavior.

No impact preview is shown in the create-mode picker because a brand
new app starts with zero users seeing it, so adding permissions cannot
hide it from anyone.

Code-review follow-up: tightened input validation so non-integer or
non-positive `permissionIds` now return 400 with a clear error instead
of being silently dropped by the previous filter. The legacy single-add
endpoint already used this exact 400 message, so behavior stays
consistent across both create and update paths.

Verification:
- `pnpm --filter @workspace/api-spec run codegen` passes.
- `pnpm --filter @workspace/api-server` typechecks with no new errors
  (executive-meetings.ts errors are pre-existing and unrelated).
- Ran the existing app-permission test suites
  (`app-permission-audit.test.mjs`, `app-permissions-crud.test.mjs`,
  `app-permissions-impact.test.mjs`) directly — all 16 tests pass.
- Ran an e2e Playwright test (login as admin → Add app → pick a
  permission → save → verify the row shows 1 restriction → reopen and
  confirm the assigned permission). All steps passed.

Follow-up proposed: automated tests for the new create-with-permissions
endpoint behavior (#231).

Replit-Task-Id: c229777c-4036-4a6a-b4cb-05ccc18f6c9b
2026-04-30 18:21:21 +00:00
riyadhafraa b507b33cad Add app-permissions impact preview before tightening an app's gate
Mirrors the existing role-permissions impact preview UX for app
permissions. Admins now see how many currently-visible users would lose
access before they add a permission requirement to an app, plus the
groups (via group_apps) that offset the loss because their members keep
access regardless.

Backend
- New endpoint POST /api/apps/:id/permissions/impact-preview in
  artifacts/api-server/src/routes/apps.ts. Implements the same OR
  semantics as getVisibleAppsForUser: a user "sees" an app if they hold
  ANY required permission (direct or via a group role) OR they belong
  to a group granted the app via group_apps. Admins are excluded from
  counts since they always see every app. Short-circuits with
  noChange:true when the candidate set equals the current set.
- OpenAPI schema (lib/api-spec/openapi.yaml): adds the path,
  AppPermissionsImpactBody, AppPermissionImpactGroup,
  AppPermissionsImpact. Regenerated lib/api-client-react bindings.

Frontend
- AppPermissionsEditor (artifacts/tx-os/src/pages/admin.tsx): debounced
  (350ms) cancel-safe preview when a pending permission is selected,
  warning banner with affected/visible counts and offsetting groups,
  and a confirmation dialog when affectedUserCount > 0. Add button is
  disabled while the preview is loading or errored to keep the warning
  trustworthy.
- i18n keys added to en.json and ar.json under
  admin.appPermissions.{impactTitle, impactLoading, impactError,
  impactNone, impactSummary, impactViaGroups, confirmTitle, confirmBody,
  confirmAction}.

Tests
- artifacts/api-server/tests/app-permissions-impact.test.mjs: 7 tests
  covering noChange short-circuit, unrestricted-app tightening,
  candidate that keeps an existing permission, group_apps offset,
  unknown app (404), invalid payload (400), and admin-only enforcement.
  All 18 app-permissions tests pass.
- E2E flow verified via runTest: admin login → /admin → Apps → edit
  app → select permission → preview banner appears → Add → confirm
  dialog → cancel without writing.

Out-of-scope (filed as follow-ups #228 and #229): listing the specific
affected user IDs in the preview, and warning when REMOVING a
permission broadens access.

No deviations from the task spec.

Replit-Task-Id: 8b2ff9ea-f95e-4bdb-b268-95b5d17154ca
2026-04-30 18:03:08 +00:00
riyadhafraa 40534b7c8f Add Playwright spec for /my-orders unmount-flush of pending deletes
Original task (#158): /my-orders has a useEffect cleanup that, on
unmount, clears every still-running undo timer and immediately fires
the deletions for whatever IDs were queued. This unmount-flush path
was previously not covered by tests — only the "Undo" path was.

Implementation:
- New spec: artifacts/tx-os/tests/order-delete-flush-on-unmount.spec.mjs
- Mirrors the test scaffolding (DB pool, user/order seeding, login,
  language init, toast locator) used by the existing order-undo-toast
  and order-clear-finished-undo specs.
- Flow: seed a completed order, log in, go to /my-orders, click trash
  + "Yes, delete" to queue the deletion, then navigate away via the
  in-page Back button (which is wouter setLocation("/services") — an
  in-SPA route change, so MyOrdersPage actually unmounts and the
  cleanup useEffect runs).
- Asserts both required outcomes from the task spec:
  * A DELETE /api/orders/:id request is observed during the
    navigation (with a 2xx response).
  * The order row is actually gone from the DB.
- Additionally records the timestamp of the DELETE request and asserts
  it fires within 3s of the navigation (well under UNDO_WINDOW_MS).
  This guards against a regression that removes the cleanup but
  accidentally still passes because the natural 7s timer eventually
  fires anyway in the SPA — without the cleanup, the DELETE would
  arrive ~7s later and the assertion would time out.
- Clean up the seeded order id from the afterAll list when the test
  successfully deletes it via the UI, so teardown does not try to
  re-delete a missing row.

No production code changed; this is a pure test addition.

Replit-Task-Id: 655f3d3b-5fc5-4a87-a754-76ec67148186
2026-04-30 17:29:14 +00:00
riyadhafraa f6078a8968 Add Playwright tests for the bulk "Clear N finished" + undo flow on /my-orders
Task #157 asked for end-to-end coverage of the bulk-delete path on
/my-orders, which goes through the same scheduleDelete() in
artifacts/tx-os/src/pages/my-orders.tsx as the single-card delete but
uses a single shared setTimeout that owns multiple order IDs and a
different (i18n plural) toast title. None of that was exercised by
the existing order-undo-toast.spec.mjs.

New file: artifacts/tx-os/tests/order-clear-finished-undo.spec.mjs

Three tests, all using the same DB-seeding + login pattern as the
existing undo-toast spec:

1. English plural + Undo: seeds 3 finished orders, clicks
   "Clear 3 finished", confirms, asserts the plural toast
   ("3 orders deleted" => myOrders.clearedCount_other), clicks Undo
   inside the 7s window, then waits past UNDO_WINDOW_MS and asserts
   that NO DELETE /api/orders/:id requests went out, all 3 rows still
   exist in the DB, and all 3 cards are visible again. This is the
   core regression guard for the multi-id shared-timer logic.

2. English plural + timer expiry: seeds 1 completed + 1 cancelled
   order (to confirm both finished states are swept), clicks
   "Clear 2 finished", asserts the plural toast, does NOT click Undo,
   polls until both DELETE responses fire and both DB rows are gone.

3. Arabic singular bulk path: seeds 1 finished order, clicks the
   Arabic "Clear 1 finished" button, confirms, asserts the singular
   toast title. NOTE: when ids.length === 1 the bulk path falls
   through to myOrders.deleted ("تم حذف الطلب"), NOT
   clearedCount_one — so the test asserts actual current behavior and
   the comment explains why. Then Undoes and verifies the row stays.

Deviation from task spec: the task said "Both the singular ('1 order
deleted') and plural ('{count} orders deleted') toast titles are
exercised at least once." The plural string is exercised, but the
literal "1 order deleted" string (clearedCount_one) is unreachable
in the current code — scheduleDelete uses myOrders.deleted for the
single-id case. I tested the actual singular branch instead and
filed follow-up #223 to either route the single-id bulk case through
clearedCount_one or remove the dead translation key. Also filed
follow-up #224 for the unused clearedPartial string (partial-failure
toast for bulk clear).

Verified locally: all 3 new tests pass via
  pnpm --filter @workspace/tx-os exec playwright test \
    tests/order-clear-finished-undo.spec.mjs
(3 passed in ~47s, exit 0). No existing files were modified.

Replit-Task-Id: ba9e23a5-d8ff-4376-ab8c-d03d6a5f15e4
2026-04-30 17:20:54 +00:00
riyadhafraa a0e12a15f7 Task #220: Insert persons mid-list + drag-reorder attendees
Original request: Add a per-section "+ شخص هنا" inline chip in the
attendees cell so users can splice a new person right after a
subheading without opening the manage dialog, and add drag-and-drop
reordering inside the manage dialog (with up/down arrows kept as a
keyboard fallback).

Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Attendee gets an optional client-only `_sid` (stripped by the
  manage-save projection so it never crosses the wire).
- AttendeeFlow now renders a "+ شخص هنا" chip right after every
  subheading row; clicking it opens the pending-add editor anchored
  to that section's tail and threads `insertAtIndex` through to
  `commitAddAttendee`, which splices + renumbers contiguously.
- Manage dialog wraps its attendee list in `DndContext` +
  `SortableContext` (vertical strategy). Each row is now a
  `SortableAttendeeRow` driven by `useSortable`; only the new
  `GripVertical` handle is wired to dnd-kit listeners so inputs,
  selects, chevrons and the delete button stay interactive.
- `reorderAttendeesByDrag(activeSid, overSid)`, memoised
  `sortableIds`, and pointer + keyboard sensors live on the manage
  section. `openEdit` and add-row helpers stamp `_sid` so each row
  has a stable React key.
- AR/EN locale keys for the chip label/aria and drag handle aria
  were already in place from the prior compaction.

Tests (new): artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
- 6/6 passing (3 scenarios × AR + EN):
  1) "+ person here" inserts at the right slot after a subheading.
  2) Drag-reorder via keyboard (Space + ArrowDown) inside the manage
     dialog persists the new order.
  3) Mixed person + subheading drag preserves `kind` and order
     across the round-trip (architect-flagged coverage).

Verification:
- 6/6 new e2e pass (45.3s).
- 6/6 existing subheading e2e still pass (Task #207 untouched).
- tx-os typecheck clean for executive-meetings.tsx (admin.tsx errors
  are pre-existing/unrelated codegen drift).
- Architect review: PASS — DnD wiring composed correctly, `_sid`
  properly stripped from save payload, no security findings.

Follow-ups proposed: #221 (assert _sid never leaks to API) and #222
(quick-add chip between any two persons, not just after subheadings).
2026-04-30 14:52:22 +00:00
riyadhafraa 8d998bf8e8 Test the merge & current-meeting tint feature end-to-end (task #154)
Adds API + UI/e2e coverage for the cell-merge overlay introduced by
task #152 on the executive-meetings schedule. Two new test files,
no production code changes.

API tests (artifacts/api-server/tests/executive-meetings-merge.test.mjs):
- Setting a merge writes mergeStartColumn/mergeEndColumn/mergeText and
  the row round-trips back through GET ?date=.
- Clearing with `merge: null` nulls all three columns while leaving the
  rest of the row intact (combined with a titleEn change in the same
  PATCH to prove only the merge fields move).
- Invalid range start>end is rejected with 400 and the row is unchanged
  (also covers an unknown enum value).
- mergeText is sanitized: <script>, onerror, javascript:, and <img>
  are stripped; visible text, <strong>, and the allowed inline color
  style (rgb(220,38,38)) survive — locks the contract that Tiptap-style
  formatting on the merged label is preserved.
- A non-mutate user (executive_viewer role) gets 403 on PATCH merge
  and a follow-up DB SELECT confirms no fields changed.

UI / e2e tests (artifacts/tx-os/tests/executive-meetings-merge.spec.mjs):
- Two-tab realtime: separate browser contexts both open the same day;
  applying a merge in tab A makes the merged cell appear in tab B
  without a manual reload (relies on the existing
  `executive_meetings_changed` Socket.IO emit + client invalidator).
- Hidden # column: with `em-schedule-cols-v1` localStorage flagging
  number as invisible, the row-actions kebab still mounts on the next
  visible cell and the Merge submenu is reachable.
- Non-contiguous reorder: a row with a stored merge spanning
  meeting+attendees is loaded after reordering columns to
  [number, meeting, time, attendees]. The merged cell is NOT rendered
  (correct fallback) but the kebab still surfaces the Unmerge action,
  and clicking it clears all three merge columns in the DB.

Both files clean up their own meeting rows + audit log entries +
seeded users in afterAll/after. All 5 API tests + 3 Playwright tests
pass against the running dev workflows.

Follow-ups proposed:
- #217 Show merged cells in the meeting PDF & archive views
- #218 Test the merge popover in Arabic / RTL

Replit-Task-Id: c696223a-209b-4111-9921-e0540e1e16a5
2026-04-30 13:43:56 +00:00
riyadhafraa 97f251bab6 Task #149: Add automated mobile/touch test for reordering meetings on iPad
Adds a new Playwright spec
`artifacts/tx-os/tests/executive-meetings-touch-reorder.spec.mjs` that
exercises the iPad/touch path of drag-to-reorder on the Executive
Meetings daily schedule — the path fixed by Task #141 (TouchSensor +
`touch-action: none` on the row grip handle) but never covered by an
end-to-end test.

What the test does
- Configures the spec's browser context with viewport 768x1024,
  hasTouch=true, isMobile=true so Chromium emulates an iPad and emits
  real TouchEvents that dnd-kit's TouchSensor can pick up.
- Seeds two adjacent meetings on a unique far-future date via direct
  DB inserts (mirrors the strategy in
  executive-meetings-schedule-features.spec.mjs) and cleans them up in
  afterAll.
- Logs in as admin via the UI, navigates to /executive-meetings, jumps
  to the seeded date, and turns on edit mode so the row grip handle
  renders.
- Drives the touch gesture through CDP `Input.dispatchTouchEvent`
  (Playwright's high-level `page.touchscreen` only exposes `tap()`):
  touchStart on the first row's grip, hold still for 350ms (≥ task's
  250ms / dnd-kit's 200ms activation delay), drag down past the second
  row in 12 sub-steps, touchEnd.
- Asserts: (1) a POST /api/executive-meetings/reorder fires AND
  succeeds, (2) the request body's `orderedIds` reflects the swap,
  (3) the DB row daily_numbers + start/end times swap as the reorder
  endpoint specifies, (4) the visible DOM row order swaps, and (5)
  after a reload the new order persists.

Wiring
- The spec lives in `artifacts/tx-os/tests/` and is auto-discovered by
  the existing playwright config (`testMatch: /.*\\.spec\\.mjs$/`), so
  it runs as part of `pnpm --filter @workspace/tx-os test:e2e` with no
  config changes.

Verification
- `pnpm --filter @workspace/tx-os test:e2e -- executive-meetings-touch-reorder.spec.mjs` → 1 passed.
- Re-ran the existing `executive-meetings-schedule-features.spec.mjs`
  (4 tests) to confirm no regression — all 4 still pass.

Follow-ups
- Proposed #214: Add iPad/touch test for reordering schedule columns
  (the file has a second DndContext for column-header drag that uses
  the same sensor stack and is currently untested on touch).

Code review feedback
- Reordered scrollIntoViewIfNeeded() to run BEFORE the boundingBox
  reads so any auto-scroll from bringing the grip on screen can't
  invalidate the touch coordinates we then dispatch via CDP. (Other
  comment about an unrelated opengraph.jpg change is platform-side,
  not part of this task's edits.)

No deviations from the task spec.

Replit-Task-Id: 3de9a478-e4a6-41db-8ed6-4feddccb3fd4
2026-04-30 12:20:50 +00:00
riyadhafraa fe736e3ff7 Task #146: Filter and paginate role permission history
Summary
- Backend: GET /api/roles/:id/audit now accepts limit (default 10, max 200),
  offset, actorUserId (0 = no filter), and from/to (YYYY-MM-DD UTC). The
  response is now a paginated envelope `{entries, totalCount, limit, offset,
  nextOffset}` instead of a bare array.
- OpenAPI: lib/api-spec/openapi.yaml updated with the new params and a new
  RolePermissionAuditList schema; client hooks regenerated via
  `pnpm --filter @workspace/api-spec run codegen`.
- Frontend: RolePermissionHistory in artifacts/tx-os/src/pages/admin.tsx is
  now self-contained — owns its own filter state, fetches the user list for
  the actor dropdown, applies actor changes immediately, and validates date
  inputs (rejecting invalid / inverted ranges).
- Pagination: switched to TRUE OFFSET PAGINATION. The first page comes from
  React Query (so live cache invalidations after a save still refresh it),
  and subsequent "Load more" clicks fetch `offset = nextOffset` imperatively
  via getRolePermissionAudit() and append the rows to local state. There is
  no client-side ceiling on how far back an admin can page; we simply stop
  showing the button when nextOffset is null. A filtersKey effect resets the
  appended pages whenever any filter (actor / from / to) changes so we never
  serve overlapping or out-of-order rows.
- i18n: added missing keys in artifacts/tx-os/src/locales/{en,ar}.json
  (historyEmptyFiltered, historyShowing, historyLoadMore, historyFilters.*,
  historyErrors.*).
- Tests: artifacts/api-server/tests/role-permission-audit.test.mjs updated
  to read the new envelope and now also covers offset-based pagination,
  actorUserId filtering (including the "0 = no filter" semantic), and
  date-range filtering (in-range / past-range / inverted / garbage). All 9
  audit tests pass; tx-os typecheck clean.
- E2E: ran the testing skill end-to-end against /admin → role edit dialog →
  history panel: created a fresh role through the UI, made 25 permission
  writes, verified 10 → 20 → 25 pagination with no duplicates and correct
  hide-on-end behaviour, filter-by-actor reset to first page, empty date
  range showed empty state, inverted dates surfaced the validation error.

Drift / notes
- Pre-existing executive-meetings tests in the `test` workflow are still
  failing — unchanged by this task.
- The Arabic language toggle isn't exposed in the page header in this build,
  so the e2e Arabic step was skipped; locale strings are in place and the
  test ids do not change with locale.
- Code review (initial pass) flagged a 200-row UI ceiling in the previous
  "growing limit" approach. Replaced with true offset pagination (described
  above) so admins can scroll back through arbitrarily long histories.

Code review follow-ups (round 2)
- Tightened parseRoleAuditUtcDate to reject impossible calendar days
  (2024-02-31, 2025-13-01, etc.) instead of silently rolling forward.
- Aligned OpenAPI: actorUserId schema is now `minimum: 0` so the spec
  matches the runtime "0 = no filter" contract; client regenerated.
- Added a targeted backend test that asserts impossible dates → 400.

Code review follow-ups (round 3)
- UX: when applied filters become invalid, the History list now hides the
  stale entries and shows the "fix filters first" hint instead, and the
  Load more button is hidden until filters are valid again. Avoids
  presenting yesterday's results as the current view.

Code review follow-ups (round 4)
- Belt-and-braces: also reset the appended history pages when the first
  page's totalCount + first-row id signature changes, so an external cache
  invalidation (e.g. another save while the dialog is still open) cannot
  leave appended pages out of sync with the refreshed first page.

Replit-Task-Id: cc3c9e83-921f-4f72-b443-79f95f6467b1
2026-04-30 10:45:07 +00:00
riyadhafraa 754a49591e Task #204: Make attendee group headers more prominent
Original ask: in the executive-meetings schedule's attendee cell, the
"Virtual / Internal / External Attendance" group labels (rendered above
each attendee group) read like stray attendee names — text-xs with em-dash
decoration. The user wanted them to stand out visually as section labels.

Changes
- artifacts/tx-os/src/pages/executive-meetings.tsx (AttendeeGroup):
  * Header now renders text-sm font-bold, navy color (#0B1E3F), centered.
  * Hairline underline (border-b border-[#0B1E3F]/15) with explicit
    print:border-b print:border-gray-400 fallback so the underline is
    preserved on print stylesheets that strip translucent borders.
  * Em-dash decoration removed from the label.
  * pointer-events-none + select-none so clicks/selection target the
    attendee names, not the label.
  * Added stable data-testid="em-attendee-group-header" for tests.

Tests
- New spec artifacts/tx-os/tests/executive-meetings-attendee-group-headers.spec.mjs:
  * Seeds its OWN multi-group meeting (2 virtual + 3 internal) on a unique
    far-future date per locale (offsets 1/2) — never collides with real
    schedule data.
  * Asserts BOTH expected localised headers appear (virtual + internal),
    and that the external header is absent.
  * Adds a regression guard: a single-group (all-internal) meeting on a
    different unique future date renders ZERO group headers.
  * Temporarily flips admin's preferred_language per-test (with try/finally
    + afterAll restore) so AuthContext does not override the locale via
    i18n.changeLanguage on auth restore.
- Existing executive-meetings-edit-toggle.spec.mjs still passes (6/6).
- All 4 new tests pass.

Drift / deviations
- The original draft test lived inline in the toggle spec and skipped
  because today's seed data has no multi-group meetings. Moved it to its
  own spec with self-seeded data per code-review feedback so coverage is
  deterministic, header assertions are tightened (require both expected
  labels, reject the external one, not "any two of three"), and a
  single-group regression guard is added.
2026-04-30 10:43:52 +00:00
riyadhafraa 24249bc511 Task #204: Make attendee group headers more prominent
Original ask: in the executive-meetings schedule's attendee cell, the
"Virtual / Internal / External Attendance" group labels (rendered above
each attendee group) read like stray attendee names — text-xs with em-dash
decoration. The user wanted them to stand out visually as section labels.

Changes
- artifacts/tx-os/src/pages/executive-meetings.tsx (AttendeeGroup):
  * Header now renders text-sm font-bold, navy color (#0B1E3F), centered.
  * Hairline underline (border-b border-[#0B1E3F]/15) with explicit
    print:border-b print:border-gray-400 fallback so the underline is
    preserved on print stylesheets that strip translucent borders.
  * Em-dash decoration removed from the label.
  * pointer-events-none + select-none so clicks/selection target the
    attendee names, not the label.
  * Added stable data-testid="em-attendee-group-header" for tests.

Tests
- New spec artifacts/tx-os/tests/executive-meetings-attendee-group-headers.spec.mjs:
  * Seeds its OWN multi-group meeting (2 virtual + 3 internal) on a unique
    far-future date per locale (offsets 1/2) — never collides with real
    schedule data.
  * Asserts BOTH expected localised headers appear (virtual + internal),
    and that the external header is absent.
  * Adds a regression guard: a single-group (all-internal) meeting on a
    different unique future date renders ZERO group headers.
  * Temporarily flips admin's preferred_language per-test (with try/finally
    + afterAll restore) so AuthContext does not override the locale via
    i18n.changeLanguage on auth restore.
- Existing executive-meetings-edit-toggle.spec.mjs still passes (6/6).
- All 4 new tests pass.

Drift / deviations
- The original draft test lived inline in the toggle spec and skipped
  because today's seed data has no multi-group meetings. Moved it to its
  own spec with self-seeded data per code-review feedback so coverage is
  deterministic, header assertions are tightened (require both expected
  labels, reject the external one, not "any two of three"), and a
  single-group regression guard is added.
2026-04-30 10:39:48 +00:00
riyadhafraa 4ace4469eb Add Playwright keyboard-editing tests for executive-meetings schedule
Task #139: cover Enter/Esc/Tab keyboard editing of time, title, and
attendee inline editors with end-to-end tests that seed via DB and
log in via UI as admin/admin123.

What this adds
- artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs
  with 7 tests:
  * Tab into time cell + Enter opens edit + valid times save via Enter
    (asserts PATCH on /api/executive-meetings/:id and DB row updated)
  * Time cell Esc restores originals and sends no PATCH
  * Title cell Enter opens edit and Enter saves via PATCH
  * Title cell Esc restores original and sends no PATCH
  * Attendee cell Enter opens edit and Enter saves via PUT on
    /api/executive-meetings/:id/attendees
  * Attendee cell Esc restores original and sends no PUT
  * Tab from title cell reaches the time cell in the same row

Implementation notes / deviations
- Each test seeds its own future-dated meeting (and attendee where
  needed) directly through the pg pool with a unique date, then
  cleans those rows in afterAll — same pattern as the existing
  schedule-features spec.
- After every save the page invalidates the day query and refetches;
  assertions wait for that GET to land before reading the DOM, which
  removed an early flaky-timing failure.
- The title save test was originally written to do select-all + type.
  We discovered (via instrumented PATCH inspection) that the
  EditableCell writes to title_ar vs title_en based on the user's
  preferredLanguage, not the visible UI direction — admin's preferred
  language is ar, so saves go to title_ar even when the schedule is
  rendered LTR. The test now appends a unique suffix and asserts
  against whichever column actually received the saved HTML, mirroring
  the schedule-features formatting test. The underlying language-write
  mismatch is captured as a follow-up.
- All 7 tests pass locally (~58s total). The pre-existing failures in
  the `test` workflow are unrelated PDF tests, not from this work.

Code-review feedback applied
- Removed two unrelated stray files (artifacts/tx-os/nohup.out and
  artifacts/tx-os/public/opengraph.jpg) that had no code references.
- Scoped both attendee selectors under the seeded row's testid
  (em-row-:meetingId) so the locator stays unambiguous even if
  another test ever shares a date.

Follow-ups proposed
- #201 (test_gaps): Shift+Tab + cross-row + ghost-row keyboard tests
- #202 (tech_debt): Save edits to the column matching the visible UI

Replit-Task-Id: cbd9619c-1475-43c6-998e-163e8e6ec94a
2026-04-30 09:30:38 +00:00
riyadhafraa b1f2bab1cd Task #138: Show a clear message when meeting end time is before start time
Original ask
- In the executive-meetings schedule, when an admin edits a meeting's
  inline time and types end < start, the change was silently discarded.
- Surface a localized toast for both the client-side guard and the
  server's 400 validation, and keep the cell in edit mode with the
  user's draft preserved so they can fix the typo without retyping.

What changed
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - TimeRangeCell.save now defers setEditing(false) until after the
    server PATCH succeeds. On a thrown TimeOrderError it stays in edit
    mode (preserving the typed draft) and refocuses the start input.
    On any other failure it rolls back to the saved values as before.
  - The parent saveTimes callback inspects the API error message; when
    it matches the server's "startTime must be <= endTime" validation,
    it shows the same localized timeOrderError toast (instead of the
    raw English error string) and rethrows a tagged Error with
    name "TimeOrderError" so TimeRangeCell can react.
- artifacts/tx-os/src/locales/en.json + ar.json
  - Updated the existing executiveMeetings.schedule.timeOrderError
    keys to the exact wording from the task spec:
      EN: "End time is before start time"
      AR: "وقت النهاية قبل وقت البداية"

Notes / non-changes
- The client-side guard at the top of TimeRangeCell.save already
  emitted the toast and returned without exiting edit mode, so it only
  needed the new copy. The change to defer setEditing(false) avoids a
  race where the !editing useEffect would wipe the draft to startSaved
  during the network round-trip on the server-side error path.
- No new i18n keys were added; existing keys were repurposed with the
  spec's exact wording.

Verification
- pnpm tsc on tx-os: no new TypeScript errors in executive-meetings.tsx
  (pre-existing errors in admin.tsx / use-notifications-socket.ts are
  unrelated to this task).
- e2e Playwright test: created a meeting at 10:00–11:00, opened the
  inline editor, set 09:00 / 08:00 and clicked save → destructive
  toast "وقت النهاية قبل وقت البداية" appeared, inputs stayed visible
  with the typed values intact. Corrected end to 10:00 and saved →
  cell exited edit mode showing 09:00 – 10:00 and the API persisted
  the new times.

Replit-Task-Id: f701faa8-02a9-4389-b130-92e522744128
2026-04-30 08:35:24 +00:00
riyadhafraa 0fe28fe16d Update toggle labels and restore image
Update locale keys for toggle functionality from 'editToggle' to 'saveToggle' and revert 'opengraph.jpg' to its previous state.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 05fe07ec-5152-4ad6-985f-c842d7501206
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fEDT27c
Replit-Helium-Checkpoint-Created: true
2026-04-30 08:35:01 +00:00
riyadhafraa 6abc60deaa Task #198: Edit→Save toggle + bulk-clear attendees + multi-select delete meetings
Three executive-meetings polish features on the Schedule view:

1. Edit-mode toggle now flips its label (Edit↔Save) and icon
   (Pencil↔Check) so a second click reads as "I'm done editing."
2. New-/edit-meeting dialog gained a "Remove all attendees" button
   next to "+ Add attendee" with a confirm prompt; only renders
   when ≥1 attendee is present.
3. Per-row select checkboxes (only in edit mode) drive a tri-state
   "select all" checkbox in the schedule header AND a floating
   bulk toolbar that appears only when ≥1 row is selected. The
   toolbar shows "Selected N of M" with "Delete selected" +
   "Clear selection". Single confirm wipes all checked meetings.

Implementation notes:
- Bulk-select checkbox is rendered as an absolute overlay on the
  first visible non-merged cell of every row (and on the merged
  <td> for merged rows) instead of being inlined inside the # cell.
  This (a) preserves the # cell's grip handle so dnd-kit drag still
  works, (b) keeps the checkbox reachable when users hide # via the
  column customizer, and (c) keeps it reachable on rows whose merge
  swallows the leading cells.
- Tri-state "select all" is rendered as an absolute overlay on the
  first visible <th> in <thead> (typically # but falls back to the
  next visible header when # is hidden).
- Floating bulk toolbar is gated on `selectedMeetingIds.size > 0`
  per spec — it disappears entirely with zero selection.
- Selection clears on date change, edit-mode-off, post-bulk-delete,
  AND on every successful day refresh (detected via meetings array
  reference change from useQuery), so a refresh that returns the
  same ids cannot leave a stale selection.
- Locale text added to ar.json/en.json for saveToggle.*, bulk*,
  removeAll, schedule.bulkSelectRow.

Tests:
- New spec executive-meetings-bulk-actions.spec.mjs (5 tests):
  edit toggle flip, dialog remove-all-attendees, multi-select
  delete with DB verify + tri-state header indeterminate/unchecked
  state assertions, hidden-# overlay survival, merged-row overlay
  survival. All pass.
- Verified no regressions in executive-meetings-edit-toggle (4/4),
  -row-actions-menu (2/2), -schedule-features (4/4, including
  drag-row reorder), -manage-create (1/1).

Code review (architect, 3 rounds):
- Round 1 flagged checkbox-in-#-cell breaks when # is hidden →
  fixed by lifting to absolute overlay.
- Round 2 flagged merged rows skipped overlay → fixed by sharing
  the overlay JSX between renderCell (first unmerged cell) and the
  merged <td>, plus added the merged-row e2e regression test.
- Round 3 (validation) flagged: (a) toolbar visible without
  selection, (b) tri-state should be in <thead>, (c) selection
  should reset on day refresh. All three addressed; tests updated.

Pre-existing failures unrelated to this task: 2 PDF tests in
api-server (Tasks #172/#179) — untouched.
2026-04-30 08:30:39 +00:00
riyadhafraa 0062430b31 Show deleted entity name beside target id in admin Audit Log
Task #133: Admin Audit Log rows now display the deleted item's
human-readable name next to its `<type> #<id>` target reference
without admins having to expand the JSON metadata.

Changes
- artifacts/tx-os/src/pages/admin.tsx
  - New helper `forceDeletedEntityName(entry, lang)` that pulls a
    localized display name from a force-delete row's metadata.
    Priority: nameEn/nameAr/displayNameEn/displayNameAr (lang-aware)
    > plain `name` > `@username`.
  - `AuditLogRow` renders an additional muted "Target: <type> #<id>
    · <name>" line beneath the summary for force-delete entries that
    expose a name. When no summary is generated, the existing target
    fallback line is upgraded to include the name as well.
- artifacts/tx-os/src/locales/en.json, ar.json
  - New `admin.audit.targetWithName` key with English and Arabic
    translations.

Behavior
- Names respect the active language (Arabic preferred when lang=ar,
  with graceful fallback to the other locale, then `name`, then
  `@username`).
- Non-deletion rows and deletion rows whose metadata lacks a name
  are unchanged — no extra line is rendered, so there is no
  regression for existing entries.

Verification
- `pnpm exec tsc --noEmit` in artifacts/tx-os passes after running
  `pnpm --filter @workspace/api-spec run codegen`.
- E2E test (Playwright) confirms the new line renders for both a
  service.force_delete row (English-only metadata) and an
  app.delete force row (English + Arabic metadata), and that the
  Arabic UI surfaces the Arabic name when present and falls back to
  the English name when not.

Follow-ups proposed
- #194 Persist user display names in user.delete audit metadata.
- #195 Log every service deletion, not only forced ones.

Replit-Task-Id: cd312541-1c90-4849-afd6-e6757aedfe06
2026-04-30 06:52:21 +00:00
riyadhafraa 5c21bf9737 Improve test to accurately verify meeting row actions menu
Refactor e2e test assertion for executive meeting row actions menu to use a more robust check for kebab visibility and count.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 4624aae4-b60d-45dd-aeaa-32ff564cc1b9
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/wd2kz0e
Replit-Helium-Checkpoint-Created: true
2026-04-30 06:27:40 +00:00
riyadhafraa 311d59ccad Task #191: Consolidate row-cell overlay icons into single kebab menu
The schedule's per-row affordances (delete, row color, merge cells)
used to render as three separate hover-revealed icon buttons stacked
inside the same narrow cell, which collided visually on iPad and
small screens. Replaced them with a single MoreVertical kebab trigger
that opens a Radix popover with three views (main / color / merge),
plus a "Back" affordance to return from sub-views to the main menu.

Implementation:
- New RowActionsMenu component (artifacts/tx-os/src/pages/executive-meetings.tsx)
  rendered once per row, anchored at the trailing-top of the first
  visible cell (matching the previous overlay anchor logic). Resets
  to "main" view on close.
- Old DeleteRowButton, RowColorPicker, and MergeMenu components removed.
- Existing testids preserved on the popover items so external tests
  still target em-delete-row-{id}, em-row-color-trigger, and
  em-merge-trigger-{id}. New testids added: em-row-actions-{id} for
  the kebab trigger, em-row-actions-back for the Back button.
- Locale keys added in ar.json/en.json: rowActions.label, rowActions.back.
- New imports: MoreVertical, ChevronLeft, ChevronRight (RTL-aware Back arrow).
- Merged-cell branch also uses RowActionsMenu so merge/unmerge stays
  reachable when the # column is hidden.

Tests:
- New regression spec: executive-meetings-row-actions-menu.spec.mjs
  exercises the full menu flow (open → color → back → main → merge → escape).
- Existing executive-meetings-edit-toggle.spec.mjs and
  executive-meetings-schedule-features.spec.mjs continue to pass —
  legacy testids remain absent in view mode and present inside the
  popover when opened in edit mode.

Pre-existing test failures (NOT caused by this change):
- "non-mutate user (executive_viewer)" spec fails because seeded user
  "ahmed" is missing from the dev DB (parallel task #131 drizzle work).
- TS errors in admin.tsx tracked by other tasks (api-client codegen
  out of sync).

Follow-up proposed:
- #192: show row color + merge previews inline in the main menu items.
2026-04-30 06:24:57 +00:00
riyadhafraa 6908207df5 Show dependency counts inline in admin lists (Task #128)
Surfaces the dependency counts (already returned by the admin list
endpoints since Task #96) directly in the Apps, Services, and Users
admin panel rows so admins can see usage at a glance — no need to open
the delete dialog to find out.

Changes:
- artifacts/tx-os/src/pages/admin.tsx:
  - Apps panel rows now render a small bullet-separated subtitle line
    under the route showing non-zero groupCount / restrictionCount /
    openCount (data-testid="app-counts-<id>").
  - Services panel rows render an inline "N orders" line under the
    price when orderCount > 0 (data-testid="service-counts-<id>").
  - Users panel rows render a bullet-separated subtitle under the
    displayName showing non-zero noteCount / orderCount /
    conversationCount / messageCount (data-testid="user-counts-<id>").
  - Style mirrors the existing GroupsPanel inline counts row
    (text-[11px] muted-foreground, flex flex-wrap, "•" separators).
  - Zero counts are filtered out so empty rows stay clean.

- artifacts/tx-os/src/locales/{en,ar}.json:
  - Added admin.apps.counts.{groups,restrictions,opens}
  - Added admin.services.counts.orders
  - Added admin.users.counts.{notes,orders,conversations,messages}

- lib/api-client-react/dist + tsbuildinfo: regenerated stale composite
  build output so the count fields on UserProfile / App / Service
  schemas (added in Task #96) are visible to the tx-os typecheck.
  No source change in lib/api-client-react.

Verification:
- tsc --noEmit passes cleanly for artifacts/tx-os.
- End-to-end browser test confirmed: admin sees inline counts in Apps,
  Services, and Users; rows with zero deps render no counts row;
  Arabic/RTL layout still works.

Replit-Task-Id: 31d3e38d-f611-4d5e-9cfa-823326495328
2026-04-29 20:16:37 +00:00
riyadhafraa 4e9cb38ab1 Update meeting scheduling button text for clarity
Correct the text for the "Add meeting" button in both Arabic and English locale files to remove unnecessary characters.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b75246ad-35b1-43b5-a2df-48c01dd0337b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/wYObB6M
Replit-Helium-Checkpoint-Created: true
2026-04-29 19:49:13 +00:00
riyadhafraa b4d66bc0f2 Task #114: Readable audit log summaries for delete/update entries
- artifacts/api-server/src/routes/groups.ts:
  - Added loadSubResourceNameFields() to fetch username / appSlug+nameEn+nameAr / roleName when adding or removing a group sub-resource.
  - POST/DELETE /groups/:id/:kind/:targetId now persist these names alongside the id in audit metadata. Best-effort lookup on DELETE so a missing linked record does not break the action.
  - Removed unused SUB_TABLE constant.

- artifacts/tx-os/src/pages/admin.tsx:
  - formatAuditSummary now renders human-readable lines for:
    - app.update rename (changes.nameEn/nameAr/slug from→to)
    - group.update rename (previousName, when only one field changed)
    - group.user/app/role.add/remove (prefer enriched name fields, fall back to *ById locale keys with #id when only the id is known)
    - settings.update registrationOpen toggle ("Opened/Closed public registration", with optional "with N other change(s)" suffix)
  - Helper linkedAppName() shared by the app branches.
  - Raw JSON metadata remains available via the existing expand toggle.

- artifacts/tx-os/src/locales/{en,ar}.json:
  - Added admin.audit.summary keys for app.rename, group.rename, group.{user,app,role}{Add,Remove}/{Add,Remove}ById, settings.registrationOpened/Closed/OpenedWith/ClosedWith in both English and Arabic.

Verification:
- Backend metadata enrichment validated end-to-end (group create, user/app/role add+remove, group rename, settings toggle) via a temporary script — all rows persist the new name fields.
- Browser e2e test logged in as a freshly created admin, exercised the same flows, opened the Audit log panel, and confirmed each row renders the readable summary (no #id placeholders) and that the raw JSON pane still expands and contains 'username'.

Notes / drift:
- The task brief listed many actions (user.delete, role.delete, app.delete, app.create, etc.). The group sub-resource actions and rename / registration toggle branches were the ones that previously rendered as opaque "#id" text and are addressed in this task. Top-level user/app/role delete already had decent metadata; further enrichment proposed as a follow-up so it can be reviewed independently.

Replit-Task-Id: 16fe1927-3329-4c15-b1ae-5fe10869aed0
2026-04-29 19:07:01 +00:00
riyadhafraa 7877cb173f Schedule attendees: force number+name onto the same visual line
Task #175. Follow-up to #173. The first fix added `whitespace-nowrap`
on each attendee `<li>`, but users still saw the index span (`1-`,
`2-`) stacked above the name — even with very short names like
"رياض" / "محمد" that obviously fit on one line. Two screenshots
(before and after the merge) showed the same stacked layout, ruling
out narrow-column wrapping.

Root cause: attendee names are saved as tiptap HTML such as
`<p>محمد</p>`. Inside the inline-block EditableCell shell (and even
inside the plain view-mode `<span>`), the default block-level `<p>`
with its 1em top/bottom margins forced the name onto its own visual
row beneath the index span. `whitespace-nowrap` cannot pull a block
child back onto the parent line.

Fix (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Each attendee `<li>` is now `inline-flex items-baseline
  whitespace-nowrap` so the index span and the name wrapper become
  flex children that structurally cannot break apart.
- The view-mode `<span>` (plain dangerouslySetInnerHTML) gets
  `[&_p]:inline [&_p]:m-0` — tiptap `<p>` renders as inline with no
  margins.
- The editable EditableCell wrapper gets the more-scoped
  `[&>span_p]:inline [&>span_p]:m-0`, which matches only the
  view-mode shell `<div> > <span> > <p>` and deliberately does NOT
  match the editing shell `<div> > <div(border)> > EditorContent`,
  so pressing Enter inside the editor still creates a real new
  paragraph.

Tests (artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs):
- New regression spec asserts that the index span and the name
  wrapper share the same vertical center (within 8px) for the first
  attendee in BOTH view mode and edit mode. Without the fix the
  centers differ by a full line height (~20px+).
- The new spec is parameterised over `tx-lang` so it runs once for
  English (LTR) and once for Arabic (RTL) — the bug originally
  surfaced on the Arabic schedule, so RTL coverage matters.
- The new spec self-skips (rather than fails) if the schedule has
  no attendees, so an empty environment doesn't masquerade as a
  layout regression.
- All passing.

Out of scope (unchanged): grouping/sorting, index format, multi-
group Virtual/Internal/External rows, pending +Add ghost row,
other EditableCell call sites (title, time, notes, manage tab).
2026-04-29 19:04:00 +00:00
riyadhafraa 1e221754b3 Adjust time display to show 24-hour format left-to-right
Modify time formatting to use 24-hour clock and enforce left-to-right display for time ranges in executive meetings.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 78b494e1-a814-4fb6-8646-6932627fdbab
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/2GpAnn7
Replit-Helium-Checkpoint-Created: true
2026-04-29 18:27:05 +00:00
riyadhafraa 11aaaf2abe Replace executive-meetings print-to-PDF with server-side PDF generator
The "Download PDF" button on the executive-meetings page now hits a real
backend endpoint that returns a true PDF (no more browser print dialog),
respects each user's font preferences (family, size, weight, alignment)
with proper Arabic RTL shaping, and archives every download.

The renderer maps each saved fontFamily ("system", "Cairo", "Tajawal",
"Noto Naskh Arabic", "Amiri") to a concrete pair of bundled font files
so the chosen family genuinely changes the embedded glyphs — Cairo and
Tajawal pick Noto Sans Arabic, the Naskh-style families and the system
default pick Noto Naskh Arabic, and Latin glyphs render in DejaVu Sans
across the board. Headers, body cells, and footer all flow through the
same script-aware font selection.

Backend (artifacts/api-server)
- New GET /api/executive-meetings/pdf?date=&lang= route in
  src/routes/executive-meetings.ts that fetches the day's meetings +
  attendees, renders a PDF, uploads it to object storage, writes an
  executive_meeting_pdf_archives row (date, generated_by, byte_size,
  storage_url), and streams the file back inline.
- New src/lib/pdf-renderer.ts using pdfkit + bidi-js with bundled
  Noto Naskh Arabic and DejaVu Sans fonts in assets/fonts/.
- Added byte_size column on executive_meeting_pdf_archives (also in
  lib/db schema) and rebuilt lib/db.
- Added ambient types for bidi-js; installed @swc/helpers to satisfy
  fontkit at runtime.
- build.mjs now copies pdfkit's data/ folder (Helvetica.afm, etc.)
  into dist/data so the bundled server can construct PDFDocument.

Frontend (artifacts/tx-os)
- PdfSection in src/pages/executive-meetings.tsx now renders a single
  "Download PDF" button that fetches the endpoint, builds a Blob, and
  downloads it. Removed the print/archive-creation buttons.
- Archive list shows a Download button for new /objects/... rows and a
  read-only "Legacy snapshot" badge for older print: rows.
- Added byteSize on PdfArchive + size formatting; updated en/ar locales.

Tests
- New test "PDF GET /executive-meetings/pdf returns a real PDF and
  archives it" in tests/executive-meetings.test.mjs covers: bad-date
  400, unauthenticated 401, real %PDF body + content-type/disposition,
  archive row with byteSize/generatedBy/filePath, empty-day handling,
  and font-family mapping (Cairo embeds NotoSansArabic; Noto Naskh
  Arabic embeds NotoNaskhArabic).
- All 23 executive-meetings tests pass.

Rebase
- Rebased onto main-repl/main (37255b7 "Update the website's shared
  image"). The only conflict was the binary asset
  artifacts/tx-os/public/opengraph.jpg — accepted the incoming/main
  version since it's unrelated to this PDF work.

Drift
- Kept the legacy /executive-meetings/print SPA route and the existing
  POST /pdf-archives endpoint to preserve old archive snapshots and
  the existing snapshot test. Proposed follow-up #169 to clean these
  up once stakeholders confirm.

Replit-Task-Id: 68914058-ebd6-4670-a785-c0084fe1fc94
2026-04-29 18:01:19 +00:00
riyadhafraa 37255b7eaa Update the website's shared image
Replace the existing shared image with an updated version.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2b1d3400-ee06-468b-9b14-45d90e2d90bc
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/ZpWlWse
Replit-Helium-Checkpoint-Created: true
2026-04-29 17:49:31 +00:00
riyadhafraa 5f61ab7bf1 Send executive-meeting notifications via email + in-app alerts
Wired the Executive Meetings module to actually deliver notifications
when meeting/request/task events happen, instead of just storing
scheduled-notification rows.

Backend (artifacts/api-server):
- New helper `lib/executive-meeting-notify.ts`:
  - `recordExecutiveMeetingNotifications` inserts rows into both
    `executive_meeting_notifications` (the page's Notifications tab)
    and the global `notifications` table (the bell), inside the
    caller's transaction. Self-notifications are excluded; recipients
    are deduped.
  - `broadcastExecutiveMeetingNotifications` emits Socket.IO
    `notification_created` to each recipient's `user:${id}` room and
    one `executive_meeting_notifications_changed` global event. Called
    after the surrounding transaction commits.
  - `getUserIdsForRoleNames` resolves role holders via direct
    `user_roles` and indirect `group_roles` -> `user_groups`.
  - `sendExecutiveMeetingEmail` is a best-effort side-channel that
    logs an outbox entry when SMTP_HOST is unset (no nodemailer
    dependency added yet — see follow-up task).
  - `getUserDisplay` resolves bilingual display names with username
    fallback for use in notification titles/bodies.
- `routes/executive-meetings.ts` wired notifications into:
  - POST /executive-meetings (notify approvers — meeting_created)
  - POST /executive-meetings/requests and POST
    /executive-meetings/:id/requests (notify approvers + email outbox
    — request_submitted)
  - PATCH /executive-meetings/requests/:id (notify requester —
    request_approved/rejected/needs_edit; if approved with assignee,
    notify assignee — task_assigned)
  - POST /executive-meetings/tasks (notify assignee — task_assigned)
  - PATCH /executive-meetings/tasks/:id (reassign -> task_assigned;
    completion -> task_completed to original requester + previous
    assignee)

Frontend (artifacts/tx-os):
- `hooks/use-notifications-socket.ts` listens for
  `executive_meeting_notifications_changed` and invalidates the
  notifications/requests/tasks query keys so the page re-fetches in
  real time.
- `locales/en.json` + `locales/ar.json`: replaced placeholder intro
  with the real description and added type labels for the seven new
  notification types.

Verification:
- Restarted the API server (clean build).
- HTTP integration test: logged in as admin, created a meeting,
  submitted a request, approved the request — confirmed
  `executive_meeting_notifications` and `notifications` rows were
  inserted with correct counts (7 admins, actor excluded), the
  request_submitted email outbox log fired with bilingual subject/
  body and 6 deliverable email recipients, and self-notifications
  were correctly suppressed when actor == requester == reviewer.

No deviations from the original plan. Email delivery, per-user
notification preferences, and automated tests for the fan-out logic
are tracked as follow-ups.

Replit-Task-Id: accea784-663c-4b63-a492-8e20d648eb4c
2026-04-29 17:22:20 +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
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
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
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
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 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