Commit Graph

412 Commits

Author SHA1 Message Date
Riyadh 4febc0918a Task #334: sticky bulk-action toolbar on Manage tab
User feedback: "سو فريز هنا لأني نزلت تحت وما شفتها" — the
Manage tab's bulk-action toolbar (added in #332) was not
visible after scrolling down through the meetings list.

- Adds a `headingRef` + `ResizeObserver` effect in
  `ManageSection` that publishes the live height of
  `em-manage-heading-bar` as a `--em-manage-heading-h` CSS
  var on the `[data-em-root]` element. Mirrors the Schedule
  tab's existing pattern at L2155 for `--em-heading-h`.
  Distinct var name avoids collision when switching tabs;
  cleared on unmount so the Schedule tab doesn't inherit a
  stale offset.
- Updates `em-manage-bulk-toolbar` to `position: sticky`
  with `top: calc(var(--em-header-h) + var(--em-manage-heading-h))`,
  matching horizontal bleed (`-mx-3 sm:-mx-6 px-3 sm:px-6`)
  and `#f4f6fb` page-bg of the heading bar so the two visually
  stack as a single header unit. Drops the rounded card style,
  uses a bottom-border + drop-shadow for the seam.
- Adds `print:hidden` for parity with the Schedule
  counterpart so the toolbar doesn't leak into PDF exports.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this
change. No behavior change to the Schedule tab.
2026-05-03 11:36:20 +00:00
Riyadh 29cd5575a8 Task #334: sticky bulk-action toolbar on Manage tab
User feedback: "سو فريز هنا لأني نزلت تحت وما شفتها" — the
Manage tab's bulk-action toolbar (added in #332) was not
visible after scrolling down through the meetings list.

- Adds a `headingRef` + `ResizeObserver` effect in
  `ManageSection` that publishes the live height of
  `em-manage-heading-bar` as a `--em-manage-heading-h` CSS
  var on the `[data-em-root]` element. Mirrors the Schedule
  tab's existing pattern at L2155 for `--em-heading-h`.
  Distinct var name avoids collision when switching tabs;
  cleared on unmount so the Schedule tab doesn't inherit a
  stale offset.
- Updates `em-manage-bulk-toolbar` to `position: sticky`
  with `top: calc(var(--em-header-h) + var(--em-manage-heading-h))`,
  matching horizontal bleed (`-mx-3 sm:-mx-6 px-3 sm:px-6`)
  and `#f4f6fb` page-bg of the heading bar so the two visually
  stack as a single header unit. Drops the rounded card style,
  uses a bottom-border + drop-shadow for the seam.
- Adds `print:hidden` for parity with the Schedule
  counterpart so the toolbar doesn't leak into PDF exports.

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this
change. No behavior change to the Schedule tab.
2026-05-03 11:35:13 +00:00
Riyadh 407b79d436 Task #332: port search + bulk delete/duplicate to Manage tab
Brings the Schedule tab's in-day search (#329) and bulk
delete/duplicate-to-date (#330) into the Manage tab of
executive-meetings.tsx (`ManageSection`).

- Adds `searchQuery` state + search input in the manage heading
  bar; reuses module-level `normalizeForSearch` /
  `meetingMatchesSearch` helpers and the same "reset on date
  change" semantics as the Schedule tab.
- Adds `selectedMeetingIds` Set + tri-state "select all
  visible" header checkbox + per-row checkbox column. Action
  handlers always intersect the selection set with the visible
  (filtered) rows, matching Schedule's safety pattern.
- Adds bulk-delete (with confirm + aggregated toast via
  `Promise.allSettled`) and bulk-duplicate-to-date (separate
  Dialog mirroring the Schedule one).
- Bumps loading/empty `colSpan` from 4 → 5 for the new
  checkbox column.
- Reuses existing `executiveMeetings.schedule.*` locale keys
  (already present in both ar.json + en.json from #329/#330);
  no new strings were needed.
- New testids: `em-manage-search`, `em-manage-search-clear`,
  `em-manage-select-all`, `em-manage-row-select-{id}`,
  `em-manage-bulk-toolbar`, `em-manage-bulk-selection-count`,
  `em-manage-bulk-clear-selection`, `em-manage-bulk-duplicate`,
  `em-manage-bulk-duplicate-{date,confirm}`,
  `em-manage-bulk-delete`.

Out of scope per plan (deferred): undo for bulk-delete on
Manage, cross-date "select all on this date".

`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Architect
review passed. Pre-existing api-server / test workflow
failures are unrelated to this change.
2026-05-03 11:30:44 +00:00
Riyadh 524dff188e Remove unnecessary confirmation message after acknowledging alerts
Remove the `doneToast` locale keys and the toast notification call in `upcoming-meeting-alert.tsx`.
2026-05-03 11:13:41 +00:00
Riyadh 7210d87939 Add functionality to duplicate multiple selected meetings to a future date
Adds a bulk duplicate option to the toolbar, including a dialog for selecting the target date, and implements the API calls and state management for this feature.
2026-05-03 11:03:25 +00:00
Riyadh 38d5dcf267 Update toolbar to show only visible selected items
Memoizes and displays the count of currently visible selected meetings in the bulk action toolbar, ensuring actions align with displayed data.
2026-05-03 11:00:16 +00:00
Riyadh 213d6fa9db #329 Add a meeting search box to the schedule toolbar
Lets users find a meeting in the day's schedule by typing part of its
title, an attendee name, or other text — instead of scrolling.

artifacts/tx-os/src/pages/executive-meetings.tsx
- Added two top-level helpers near translateSaveError:
  - normalizeForSearch(input): strips HTML tags + entities, drops
    Arabic diacritics + tatweel, folds common letter variants
    (آأإٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي), lowercases.
  - meetingMatchesSearch(meeting, needle): tests title (AR/EN),
    attendee names + titles, location, notes, and merge text via
    normalized substring containment.
- ScheduleSection: added searchQuery state, useEffect that resets it
  whenever the active `date` changes, normalizedSearch memo, and a
  filteredMeetings memo derived from orderedMeetings.
- Switched the row render path to filteredMeetings: SortableContext
  items, the .map render, and the select-all overlay's "anything to
  select" check now read from the filtered list. The
  setAllMeetingsSelected and allSelectedState helpers also operate
  on filteredMeetings so the header checkbox + select-all reflect
  what's actually visible.
- Added a new "no matches" empty-state row that appears only when
  the day has meetings but the active search filters them all out
  (the original "no meetings today" row is preserved for empty days).
- New search Input + clear-X button placed in the schedule toolbar,
  next to the existing date picker. Hidden in print so PDFs export
  the full schedule.
- MeetingRow gained an optional `displayNumber` prop. The "#" cell
  now renders displayNumber when provided, falling back to the
  server's dailyNumber. The schedule passes idx+1, so visible rows
  always show 1, 2, 3… without gaps when the list is filtered.

artifacts/tx-os/src/locales/ar.json + en.json
- New keys under executiveMeetings.schedule: searchPlaceholder,
  searchAria, searchClear, searchNoMatches.

- Bulk delete now intersects the selection with filteredMeetings at
  action time. The selection set is intentionally NOT auto-pruned
  when the search narrows (so clearing the search restores the
  user's earlier picks), but destructive actions only ever target
  rows the user can currently see.
- Print/PDF export ignores the active search. An `isPrinting` flag
  driven by beforeprint/afterprint + a `print` media-query listener
  swaps the table to the unfiltered orderedMeetings while printing,
  so generated PDFs always contain the full day's schedule even
  when the user has typed into the search box.

Out of scope: cross-day search, prev/next-day chevron buttons,
search history, fuzzy matching.

Verified: `tsc --noEmit` clean.
2026-05-03 10:58:15 +00:00
Riyadh 9fee952f9a #329 Add a meeting search box to the schedule toolbar
Lets users find a meeting in the day's schedule by typing part of its
title, an attendee name, or other text — instead of scrolling.

artifacts/tx-os/src/pages/executive-meetings.tsx
- Added two top-level helpers near translateSaveError:
  - normalizeForSearch(input): strips HTML tags + entities, drops
    Arabic diacritics + tatweel, folds common letter variants
    (آأإٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي), lowercases.
  - meetingMatchesSearch(meeting, needle): tests title (AR/EN),
    attendee names + titles, location, notes, and merge text via
    normalized substring containment.
- ScheduleSection: added searchQuery state, useEffect that resets it
  whenever the active `date` changes, normalizedSearch memo, and a
  filteredMeetings memo derived from orderedMeetings.
- Switched the row render path to filteredMeetings: SortableContext
  items, the .map render, and the select-all overlay's "anything to
  select" check now read from the filtered list. The
  setAllMeetingsSelected and allSelectedState helpers also operate
  on filteredMeetings so the header checkbox + select-all reflect
  what's actually visible.
- Added a new "no matches" empty-state row that appears only when
  the day has meetings but the active search filters them all out
  (the original "no meetings today" row is preserved for empty days).
- New search Input + clear-X button placed in the schedule toolbar,
  next to the existing date picker. Hidden in print so PDFs export
  the full schedule.
- MeetingRow gained an optional `displayNumber` prop. The "#" cell
  now renders displayNumber when provided, falling back to the
  server's dailyNumber. The schedule passes idx+1, so visible rows
  always show 1, 2, 3… without gaps when the list is filtered.

artifacts/tx-os/src/locales/ar.json + en.json
- New keys under executiveMeetings.schedule: searchPlaceholder,
  searchAria, searchClear, searchNoMatches.

- Bulk delete now intersects the selection with filteredMeetings at
  action time. The selection set is intentionally NOT auto-pruned
  when the search narrows (so clearing the search restores the
  user's earlier picks), but destructive actions only ever target
  rows the user can currently see.

Out of scope: cross-day search, prev/next-day chevron buttons,
search history, fuzzy matching.

Verified: `tsc --noEmit` clean.
2026-05-03 10:56:54 +00:00
Riyadh a3a9c4fac4 #326 Show meeting save errors in Arabic, centered at the bottom
Two small UX fixes for the Add/Edit Meeting save toast.

artifacts/tx-os/src/pages/executive-meetings.tsx
- Added a `translateSaveError(rawMsg, t)` helper near the other
  top-level helpers (~line 425). It substring-matches the API's
  Zod-generated message "startTime must be <= endTime" and returns
  the translated copy from
  `executiveMeetings.manage.errors.endTimeBeforeStart`. Anything
  unknown falls through to the raw server message so unexpected
  errors aren't silently hidden.
- The save catch block now passes the message through this helper
  before showing the toast. Title still uses
  `errors.saveFailed`.

artifacts/tx-os/src/locales/ar.json + en.json
- New translation key `executiveMeetings.manage.errors.endTimeBeforeStart`:
  - ar: "وقت الانتهاء يجب أن يكون بعد وقت البدء"
  - en: "End time must be after start time"

artifacts/tx-os/src/components/ui/toast.tsx
- ToastViewport: replaced the mobile-top + desktop-bottom-right
  positioning with a single bottom-center placement on every
  viewport (`fixed bottom-0 left-1/2 -translate-x-1/2`). Width
  is still capped at `md:max-w-[420px]`. The flex direction is
  `flex-col` (no longer `flex-col-reverse`) since there is now
  only one slide-in direction.
- toastVariants: dropped `data-[state=open]:slide-in-from-top-full`
  so toasts slide in from the bottom on every breakpoint, matching
  the new viewport position.

Out of scope (untouched): API validation, other dialogs/pages,
preventive client-side validation for invalid time ranges.
2026-05-03 10:17:05 +00:00
Riyadh e31a29fbbe #324 Make the Add/Edit Meeting dialog fit on a phone screen
After #322 trimmed the dialog, the Save/Cancel footer slid below
the visible 90vh sheet on phones because the whole DialogContent
was a single overflow-y-auto block.

Changes in artifacts/tx-os/src/pages/executive-meetings.tsx:

MeetingFormDialog
- Rebuilt DialogContent as a flex column: shrink-0 header,
  flex-1 min-h-0 overflow-y-auto middle (the only scroller),
  shrink-0 footer. Save/Cancel now stay pinned at the bottom
  even after many attendees are added.
- Reduced mobile padding/gaps (`p-4 sm:p-6`,
  `gap-3 sm:gap-4`, grid `gap-2 sm:gap-3`). Desktop spacing
  unchanged.
- Switched the field grid from `grid-cols-1 sm:grid-cols-2`
  to a flat `grid-cols-2` so the two short fields (التاريخ,
  الرقم اليومي) share a row even on mobile. Title spans both
  columns; time pickers span both on mobile and one each on
  sm+ via the new mobileFull variant.
- Inner scroll wrapper uses negative margins + matching padding
  so scroll content can extend edge-to-edge without losing the
  dialog's gutter.

FormRow helper
- Broadened `full` from `sm:col-span-2` to `col-span-2`
  (works in both 1-col and 2-col grids; only callers using
  `full` are in this file).
- Added `mobileFull` prop → `col-span-2 sm:col-span-1` for
  the time pickers, so AM/PM controls don't get squeezed on
  phones.

Out of scope (untouched): validation, save logic, attendee
model, removed-field defaults from #322, duplicate dialog,
audit, schedule view, translations.
2026-05-03 10:09:29 +00:00
Riyadh cfd9240a25 Update meeting form layout and Arabic title label
Refactor executive meetings form layout in `executive-meetings.tsx` to use a responsive grid, ensuring the Arabic title spans full width and other fields stack appropriately on smaller screens. Update Arabic and English locale files (`ar.json`, `en.json`) to change the Arabic title field label from "العنوان (عربي)" to "العنوان".
2026-05-03 10:00:45 +00:00
Riyadh 8f22e63f39 #322 Simplify Add/Edit Meeting dialog and attendee rows
Trim the إضافة اجتماع / تعديل اجتماع dialog and attendee rows in
artifacts/tx-os/src/pages/executive-meetings.tsx down to the fields
the user actually fills in.

MeetingFormDialog
- Removed FormRows: titleEn, location, meetingUrl, platform, status,
  isHighlighted, notes.
- Kept titleAr, meetingDate, dailyNumber, startTime, endTime, and
  the attendees section.
- Save validation now only requires titleAr.
- titleEn is always mirrored from titleAr on save. The English input
  is gone, so there is no way to keep an independent English title;
  always syncing avoids leaving a stale server-side titleEn after the
  Arabic title is edited. The API still receives a non-empty titleEn
  so POST validation (min length 1) is satisfied.
- MeetingFormState, emptyMeetingForm, and openEdit are unchanged so
  hidden fields (location, notes, platform, status, isHighlighted,
  meetingUrl) round-trip through the save body and existing data is
  preserved on edit.

SortableAttendeeRow
- Removed the parenthesized title Input and the
  internal/virtual/external Select.
- Dropped the now-unused onChangeTitle / onChangeAttendanceType
  props from the component signature and the call site.
- Existing attendees keep their stored title and attendanceType;
  new attendees added via addAttendee continue to default to
  title=null and attendanceType="internal" so the schedule's
  grouping keeps working.

Cleanup
- Removed the now-unused Textarea import (only the notes field
  used it). Switch is still imported because it is used elsewhere
  in the file.

Code review found two issues that were fixed before completion:
- Mirroring made deterministic (always sync) instead of
  blank-only fallback.
- Orphaned Textarea import removed.

No DB schema, API, schedule view, Manage list, audit, notifications,
or translation changes.
2026-05-03 09:51:36 +00:00
Riyadh a03d1bf609 #322 Simplify Add/Edit Meeting dialog and attendee rows
Trim the إضافة اجتماع / تعديل اجتماع dialog and attendee rows in
artifacts/tx-os/src/pages/executive-meetings.tsx down to the fields
the user actually fills in.

MeetingFormDialog
- Removed FormRows: titleEn, location, meetingUrl, platform, status,
  isHighlighted, notes.
- Kept titleAr, meetingDate, dailyNumber, startTime, endTime, and
  the attendees section.
- Save validation now only requires titleAr.
- titleEn is always mirrored from titleAr on save. The English input
  is gone, so there is no way to keep an independent English title;
  always syncing avoids leaving a stale server-side titleEn after the
  Arabic title is edited. The API still receives a non-empty titleEn
  so POST validation (min length 1) is satisfied.
- MeetingFormState, emptyMeetingForm, and openEdit are unchanged so
  hidden fields (location, notes, platform, status, isHighlighted,
  meetingUrl) round-trip through the save body and existing data is
  preserved on edit.

SortableAttendeeRow
- Removed the parenthesized title Input and the
  internal/virtual/external Select.
- Dropped the now-unused onChangeTitle / onChangeAttendanceType
  props from the component signature and the call site.
- Existing attendees keep their stored title and attendanceType;
  new attendees added via addAttendee continue to default to
  title=null and attendanceType="internal" so the schedule's
  grouping keeps working.

Cleanup
- Removed the now-unused Textarea import (only the notes field
  used it). Switch is still imported because it is used elsewhere
  in the file.

Code review found two issues that were fixed before completion:
- Mirroring made deterministic (always sync) instead of
  blank-only fallback.
- Orphaned Textarea import removed.

No DB schema, API, schedule view, Manage list, audit, notifications,
or translation changes.
2026-05-03 09:50:53 +00:00
Riyadh 0bad4c7ac4 Improve the manage meetings page with a sticky header and proper title formatting
Update executive-meetings.tsx to make the header sticky, render meeting titles as HTML, and remove the "Status" column.
2026-05-03 09:36:42 +00:00
Riyadh 1ca14e9d95 Center all executive meeting schedule column headers
Update alignment for all schedule column headers in executive meetings to be centered for both screen and print views.
2026-05-03 08:02:53 +00:00
Riyadh 30e6c39d58 tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the
schedule time cell) to every other inline editor on executive-meetings.tsx,
fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed
edits felt sluggish or appeared to silently drop.

executive-meetings.tsx
  * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's
    DayResponse, applies a per-meeting patch via setQueryData, and returns
    a rollback closure that re-sets the snapshot on PATCH failure.
  * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now
    write the optimistic value before awaiting the network call and
    rollback() in their catch branches.

editable-cell.tsx (3-layer blur hardening)
  1. saveEdit() failure now re-opens the editor with the user's typed
     draft preserved (mirrors #316 time-cell UX) so a server error never
     forces them to retype. The parent's optimistic cache rollback
     restores the read-only `value`; the editor itself still holds the
     draft, so flipping `editing` back on is enough.
  2. Outside-pointerdown now flushes saveEdit synchronously (capture
     phase, before the click reaches its target) instead of waiting on
     the 100 ms blur timer. Closes the gap where a fast click into
     another cell triggered a row remount before blur fired.
  3. Unmount cleanup flushes any pending blurTimerRef fire-and-forget,
     a final safety net for the case where a refetch tears down the
     row before any pointer event arrives.

editable-cell.tsx (re-entrancy guard)
  * `savingRef` short-circuits a second `saveEdit` call so the
    pointerdown flush + the 100 ms blur timer + Enter/Tab handlers
    can't race and issue duplicate PATCH/PUT requests for one edit.

tests/executive-meetings-keyboard-editing.spec.mjs (+5 tests)
  * Title repaint <300 ms with PATCH delayed 1500 ms.
  * Title PATCH 500 rolls back; editor re-opens with the typed draft
    intact; Escape cancels back to the original; DB unchanged.
  * Rapid click-switch from cell A to cell B commits BOTH PATCHes to
    the server (proves the synchronous outside-pointerdown flush).
  * Rapid Tab traversal from cell A to cell B commits BOTH edits
    EXACTLY ONCE each (proves the savingRef re-entrancy guard).
  * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses
    captured testid so the locator survives the rename).

All 5 new + 3 #316 time-editor tests green. Out-of-scope pre-existing
failures in the `test` workflow (notifications/postpone-race/row-color)
left untouched per plan.
2026-05-03 07:45:30 +00:00
Riyadh 706b11ba9e tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the
schedule time cell) to every other inline editor on executive-meetings.tsx,
fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed
edits felt sluggish or appeared to silently drop.

executive-meetings.tsx
  * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's
    DayResponse, applies a per-meeting patch via setQueryData, and returns
    a rollback closure that re-sets the snapshot on PATCH failure.
  * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now
    write the optimistic value before awaiting the network call and
    rollback() in their catch branches.

editable-cell.tsx (3-layer blur hardening)
  1. saveEdit() failure now re-opens the editor with the user's typed
     draft preserved (mirrors #316 time-cell UX) so a server error never
     forces them to retype. The parent's optimistic cache rollback
     restores the read-only `value`; the editor itself still holds the
     draft, so flipping `editing` back on is enough.
  2. Outside-pointerdown now flushes saveEdit synchronously (capture
     phase, before the click reaches its target) instead of waiting on
     the 100 ms blur timer. Closes the gap where a fast click into
     another cell triggered a row remount before blur fired.
  3. Unmount cleanup flushes any pending blurTimerRef fire-and-forget,
     a final safety net for the case where a refetch tears down the
     row before any pointer event arrives.

tests/executive-meetings-keyboard-editing.spec.mjs (+4 tests)
  * Title repaint <300 ms with PATCH delayed 1500 ms.
  * Title PATCH 500 rolls back; editor re-opens with the typed draft
    intact; Escape cancels back to the original; DB unchanged.
  * Rapid click-switch from cell A to cell B commits BOTH PATCHes to
    the server (proves the synchronous outside-pointerdown flush).
  * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses
    captured testid so the locator survives the rename).

All 4 new + 3 #316 time-editor tests green. Out-of-scope pre-existing
failures in the `test` workflow (notifications/postpone-race/row-color)
left untouched per plan.
2026-05-03 07:41:28 +00:00
Riyadh 0163e89b11 tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the
schedule time cell) to every other inline editor on executive-meetings.tsx,
fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed
edits felt sluggish or appeared to silently drop.

executive-meetings.tsx
  * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's
    DayResponse, applies a per-meeting patch via setQueryData, and returns
    a rollback closure that re-sets the snapshot on PATCH failure.
  * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now
    write the optimistic value before awaiting the network call and
    rollback() in their catch branches. Re-throw behaviour preserved
    where present so EditableCell still resets its draft on failure.

editable-cell.tsx
  * Hardened blur-commit: the cell now stashes the latest `saveEdit`
    in a ref and an unmount cleanup flushes any pending blurTimerRef
    fire-and-forget. Without this, a row remount that lands during the
    100 ms blur window (e.g. Tab into the next cell triggering an
    optimistic refetch) tore down the editor before saveEdit could run
    and silently dropped the typed change. Optimistic cache writes in
    the parent make the fire-and-forget safe.

tests/executive-meetings-keyboard-editing.spec.mjs (+3 tests)
  * Title repaint <300 ms with PATCH delayed 1500 ms.
  * Title PATCH 500 rolls back to original; DB unchanged.
  * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses
    captured testid so the locator survives the rename).

Architect review: APPROVED, no concerns. All 3 new + 3 #316 time-editor
tests green. Out-of-scope pre-existing failures in the `test` workflow
(notifications/postpone-race/row-color) untouched per plan.
2026-05-03 07:31:28 +00:00
Riyadh bafeebcfc2 Task #316: fix executive-meetings inline time editor snap-back + perceived save latency
Two bugs in the schedule time-cell editor:
1. SNAP-BACK: After saving, re-opening the editor briefly showed
   blank inputs because the React Query refetch ran behind the
   close-then-reopen sequence and the picker re-seeded from stale
   meeting props.
2. PERCEIVED LATENCY: The editor stayed open until the PATCH
   round-trip resolved, so a slow network made every save feel
   slow even though the data round-trip was the only blocker.

Fixes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- saveTimes: optimistically write the new times into the
  ["/api/executive-meetings", date] query cache (DayResponse shape:
  { date, meetings: [...] }) so the read-only display repaints
  immediately. Snapshot previous payload and roll back on PATCH
  failure. Append :00 so the cached shape matches the GET refetch.
- TimeRangeCell.save(): close the editor BEFORE awaiting the PATCH;
  set savingRef while saving so the [editing, startSaved, endSaved]
  sync effect doesn't overwrite the optimistic draft on the next
  render. On failure, re-open with the user's draft intact.
- TimeRangeCell sync effect: bail when savingRef is true OR when
  the saved refs match lastSyncedRef, preventing stale prop values
  from clobbering the optimistic state mid-flight.

Tests added (artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs):
- snap-back: save 21:30/22:45, immediately re-open, assert picker
  shows the saved values (not blank).
- perceived-latency: hold PATCH 1.5s; assert editor closes within
  800ms and the read-only display shows optimistic times.
- rollback: 500 the PATCH; assert editor re-opens with the user's
  draft intact, read-only display shows the original times after
  cancel, DB unchanged.

All 3 new tests pass; the existing time-editor tests continue to
pass (6/6 in the targeted regression sweep).

Plan: .local/tasks/task-316.md
2026-05-03 07:18:11 +00:00
Riyadh f57b49bac2 EM time picker: mobile/iPad-friendly layout + touch targets
Follow-up to task #315. The 3-control picker shipped with desktop-
sized inline controls (~22px tall) and a single-row layout that
pushed the end picker out of view on a 375px iPhone, leaving the
user with no obvious way to fill the second time field.

What changed
- src/components/time-picker-12h.tsx: bumped the "inline" size
  from h<22px text-[10px] to h-8 (32px) text-sm/text-xs across
  hour input, minute input, and AM/PM toggle so a finger tap on
  mobile/iPad lands cleanly. Added `gap-1.5` between the
  [hour:minute] group and the AM/PM toggle so a thumb-tap on the
  toggle doesn't accidentally land on the minute input. Added a
  visible focus ring color and lightened the placeholder so the
  empty fields read as "fill me in" rather than already-active.
- src/pages/executive-meetings.tsx (TimeRangeCell wrapper):
  switched from `inline-flex` (single-row, ellipsis on overflow)
  to `flex flex-wrap items-end gap-x-1.5 gap-y-1`. The end picker
  now drops onto a second line when the cell can't fit both
  side-by-side, which is the iPhone case. The "–" separator is
  hidden on narrow widths (`hidden sm:inline`) since wrapping
  makes it visually wrong.
- Bumped Start/End labels from 10px to 11px font-medium for
  better at-a-glance scanability of which time field is which —
  this was the user-reported clarity issue.
- Save/cancel icon buttons grew to 32x32 hit areas (was tiny
  p-0.5 around a 14px icon ≈ 16px clickable).
- tests/executive-meetings-keyboard-editing.spec.mjs: added a
  new test.describe block "Schedule mobile viewport (iPhone 375)"
  with `test.use({ viewport: { width: 375, height: 667 } })`. The
  test asserts: (a) all 8 picker sub-controls + save button are
  visible on iPhone, (b) each interactive control is at least
  28px tall (regression guard against shrinking touch targets),
  (c) a complete tap-driven hour+minute+AM/PM entry on both
  start and end persists 21:30–22:45 to the DB.

Validation
- Unit tests: 22/22 pass (no logic changes — only CSS + layout).
- E2E: Tab walks (desktop) + compact+PM toggle (desktop) +
  new iPhone-375 mobile test all pass.
- TypeScript: clean.
2026-05-03 06:22:54 +00:00
Riyadh c8d768c1e1 Task #315: rebuild 12h picker as 3 controls (hour + minute + AM/PM)
The first ship of #315 used a single text input + AM/PM toggle. The
spec (lines 30-32) explicitly mandates THREE controls per field:
hour input (1-12), minute input (00-59), and AM/PM toggle. This
follow-up commit refactors TimePicker12h to that shape while
keeping every behaviour from the first ship intact.

What changed
- src/lib/time-12h.ts:
  - Added `splitCanonicalForPicker()` — seeds the 3-control picker's
    separate hour and minute inputs from a canonical "HH:mm" value.
  - Added `combineSplit(hour, minute, period)` — the new combine
    path. Joins "${h}:${m}" and runs the result through
    combineTextWithPeriod, preserving every disambiguation rule
    (1-12 requires toggle, 0/13-23 unambiguous, embedded marker
    overrides toggle).
  - The hour field accepts a power-user shortcut: any separator
    (":", ".", "-", " ") or AM/PM marker letter or 3-4 digit
    compact form in the hour field means "I typed the whole time
    here, ignore the minute field". Preserves the existing
    e2e tests that fill the entire time string into one input.
  - kept formatCanonicalAs12h + combineTextWithPeriod for the
    power-user shortcut path.
- src/components/time-picker-12h.tsx: rewrote as 3 controls
  (hour input, ":" separator, minute input, AM/PM radiogroup).
  Imperative {commit, focus, select} handle still focuses the
  hour input. dirtyRef preserved to avoid prop overwrite mid-edit.
  periodRef still mirrors state for synchronous commit() reads.
- src/pages/executive-meetings.tsx: added `minuteTestId` props on
  both inline editor pickers and both manage-form pickers
  (`em-time-{start,end}-minute-${id}`,
  `em-form-{start,end}Time-minute`). All other test IDs preserved.
- src/__tests__/time-12h.test.mjs: kept the 14 tests for the
  text+period path; added 8 new tests for combineSplit covering
  split-mode (1-12 ambiguous, 13-23 unambiguous, midnight, hour
  power-user shortcut, bare-hour invalid, etc) — 22 tests pass.
- tests/executive-meetings-keyboard-editing.spec.mjs: updated
  the Tab walks test to walk 8 stops (start hour → start minute
  → start AM → start PM → end hour → end minute → end AM →
  end PM) and assert both typed values survive the focus
  changes. The TYPING_SHAPES helper still works unchanged because
  the hour field accepts full time strings via the power-user
  shortcut.
- public/opengraph.jpg: reverted to HEAD~1 (was inadvertently
  swept into the prior commit by the auto-commit; not part of
  this task).

Validation
- Unit tests: 22/22 pass.
- E2E: Tab walks + canonical 24h + no-leading-zero + 12h-with-PM
  + compact+PM-toggle all pass against the new 3-control UI.
- TypeScript: clean.
2026-05-02 11:31:37 +00:00
Riyadh 17c103a59a Task #315: 12h time picker with explicit AM/PM (executive meetings)
Replaces native <input type="time"> in the executive-meetings editor
with a TimePicker12h that always requires an explicit AM/PM choice
for any 1–12 hour, fixing the "1:15 silently saved as 01:15 (AM)
when user meant 13:15 (PM)" bug. Display stays compact 12h with no
AM/PM marker (per #292); wire format remains canonical HH:mm 24h.

What ships
- src/lib/time-12h.ts: periodFromCanonical, formatCanonicalAs12h,
  combineTextWithPeriod (returns "ambiguous" for any unmarked 1–12
  hour without a toggle pick; accepts hours 0 and 13–23 directly
  because they are unambiguous 24h shapes; typed marker always
  wins over toggle).
- src/components/time-picker-12h.tsx: forwardRef component with
  imperative {commit, focus, select} handle, dirtyRef to avoid
  prop overwrite mid-edit, dir="ltr" for stable RTL layout, and
  Enter-on-toggle = "set AND save".
- Wired into TimeRangeCell inline editor (commit-based save handles
  invalid/ambiguous/TimeOrderError as toasts and refocuses).
- Wired into MeetingFormDialog with picker refs + handleSaveClick
  validation: Save button now calls commit() on both pickers,
  blocks on invalid/ambiguous (toast + focus offending picker),
  and passes canonical values straight to the parent's save handler
  so a stale React state batch from onChange-suppression cannot
  silently persist the previous value.

Tests
- 12 unit tests in src/__tests__/time-12h.test.mjs (incl. new
  "hour 0 is unambiguous, accepted without a toggle").
- 17 e2e tests in tests/executive-meetings-keyboard-editing.spec.mjs
  (incl. "compact + PM toggle" 0115→13:15 and "Tab walks start
  input → start AM/PM toggle → end input → end AM/PM toggle").
- Existing manage-create e2e suite still green.

Locale keys (en + ar)
- executiveMeetings.timeEditor.{am,pm,periodGroupStart,periodGroupEnd}
- executiveMeetings.schedule.timeAmbiguousError

Deviations from plan
- Architect review #1 caught two issues that were both fixed:
  (1) combineTextWithPeriod treated hour 0 as ambiguous; now
      accepts 00:xx unambiguously.
  (2) MeetingFormDialog initially used pickers via value/onChange
      only, which left a silent-fallback hole because onChange
      suppresses ambiguous drafts. Added imperative refs +
      handleSaveClick validation; onSave signature changed to
      (committedStart, committedEnd) so the parent's save() can
      use the freshly-committed values directly.
- Reverted accidental vite.config.ts typo (@replit/... was missing
  the leading @).
2026-05-02 11:18:52 +00:00
Riyadh 5eeb080b5e fix(executive-meetings): keep visible drag-reorder chronological with cancelled rows
Task #311. The schedule view hides cancelled meetings, but the drag-reorder
path was operating on the raw, unfiltered list — so cancelled rows consumed
time slots and the dnd-kit indices skewed across hidden rows, leaving the
visible list out of chronological order after a drop.

Server (POST /api/executive-meetings/reorder)
- Slot-swap now operates on the in-scope (orderedIds) subset only.
  Cancelled rows keep their (startTime, endTime, dailyNumber) untouched, so
  they no longer steal slots from the visible list.
- New 400 codes:
  - cancelled_in_reorder: payload includes a cancelled meeting
  - incomplete_day: any non-cancelled meeting on the day is missing
- Audit oldValue.order now reflects the visible order the user actually saw.
- Phase-1/phase-2 negative-parking still avoids transient unique-constraint
  conflicts; cancelled rows' positive dailyNumbers cannot collide because
  slot dailyNumbers are a permutation of in-scope rows' existing values.

Client (executive-meetings.tsx reorderRows)
- Index math now derives ids from `orderedMeetings` (the visible list bound
  to SortableContext) instead of the raw `meetings` array. useCallback deps
  updated.

Tests
- artifacts/api-server/tests/executive-meetings.test.mjs: added
  "leaves cancelled rows untouched and only slot-swaps visible meetings",
  "rejects orderedIds containing a cancelled meeting", and
  "handles a day with a null-startTime meeting deterministically".
- artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs: added
  two real dnd-kit drag tests driven through the keyboard sensor (Space +
  ArrowUp + Space on the row's grip handle):
  - "Schedule drag-reorder: cancelled rows ... do not disturb the visible
    chronological order" — cancelled row keeps its slot, visible rows end
    up chronological after the drop.
  - "Schedule drag-reorder: a null-startTime row drags deterministically
    and inherits its new slot" — null-time row dragged to top inherits the
    first chronological slot; the originally-null slot shifts to the row
    that lands at the bottom.

Code-review follow-ups addressed:
- Reverted the unrelated artifacts/tx-os/public/opengraph.jpg binary change.
- Added the explicit null-startTime regressions at both API and Playwright
  drag levels per review request.
- Replaced the fetch-based UI test with real dnd-kit keyboard-sensor drags
  to fully exercise the client onDragEnd path including the patched index
  math.

All 8 reorder API tests and both new Playwright drag tests pass. Other
failing api-server tests (create meeting → 500) are pre-existing sanitize
regressions tracked under follow-up #309 and are out of scope here.
2026-05-02 09:25:37 +00:00
Riyadh 053dc68b62 fix(executive-meetings): keep visible drag-reorder chronological with cancelled rows
Task #311. The schedule view hides cancelled meetings, but the drag-reorder
path was operating on the raw, unfiltered list — so cancelled rows consumed
time slots and the dnd-kit indices skewed across hidden rows, leaving the
visible list out of chronological order after a drop.

Server (POST /api/executive-meetings/reorder)
- Slot-swap now operates on the in-scope (orderedIds) subset only.
  Cancelled rows keep their (startTime, endTime, dailyNumber) untouched, so
  they no longer steal slots from the visible list.
- New 400 codes:
  - cancelled_in_reorder: payload includes a cancelled meeting
  - incomplete_day: any non-cancelled meeting on the day is missing
- Audit oldValue.order now reflects the visible order the user actually saw.
- Phase-1/phase-2 negative-parking still avoids transient unique-constraint
  conflicts; cancelled rows' positive dailyNumbers cannot collide because
  slot dailyNumbers are a permutation of in-scope rows' existing values.

Client (executive-meetings.tsx reorderRows)
- Index math now derives ids from `orderedMeetings` (the visible list bound
  to SortableContext) instead of the raw `meetings` array. useCallback deps
  updated.

Tests
- artifacts/api-server/tests/executive-meetings.test.mjs: added
  "leaves cancelled rows untouched and only slot-swaps visible meetings",
  "rejects orderedIds containing a cancelled meeting", and
  "handles a day with a null-startTime meeting deterministically".
- artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs: added
  "Schedule drag-reorder: cancelled rows on the same day do not disturb the
  visible chronological order" — drives a real dnd-kit drag via the
  keyboard sensor (Space + ArrowUp x2 + Space on the row's grip handle),
  exercising the full client onDragEnd path including the patched index
  math.

Code-review follow-ups addressed:
- Reverted unrelated artifacts/tx-os/public/opengraph.jpg binary change.
- Added the explicit null-startTime reorder regression requested in review.
- Replaced the fetch-based UI test with a real dnd-kit keyboard-sensor
  drag, covering the client index-mapping path end-to-end.

All 8 reorder API tests and the new Playwright drag test pass. Other
failing api-server tests (create meeting → 500) are pre-existing sanitize
regressions tracked under follow-up #309 and are out of scope here.
2026-05-02 09:22:34 +00:00
Riyadh c4e650bd7b fix(executive-meetings): keep visible drag-reorder chronological with cancelled rows
Task #311. The schedule view hides cancelled meetings, but the drag-reorder
path was operating on the raw, unfiltered list — so cancelled rows consumed
time slots and the dnd-kit indices skewed across hidden rows, leaving the
visible list out of chronological order after a drop.

Server (POST /api/executive-meetings/reorder)
- Slot-swap now operates on the in-scope (orderedIds) subset only.
  Cancelled rows keep their (startTime, endTime, dailyNumber) untouched, so
  they no longer steal slots from the visible list.
- New 400 codes:
  - cancelled_in_reorder: payload includes a cancelled meeting
  - incomplete_day: any non-cancelled meeting on the day is missing
- Audit oldValue.order now reflects the visible order the user actually saw.
- Phase-1/phase-2 negative-parking still avoids transient unique-constraint
  conflicts; cancelled rows' positive dailyNumbers cannot collide because
  slot dailyNumbers are a permutation of in-scope rows' existing values.

Client (executive-meetings.tsx reorderRows)
- Index math now derives ids from `orderedMeetings` (the visible list bound
  to SortableContext) instead of the raw `meetings` array. useCallback deps
  updated.

Tests
- artifacts/api-server/tests/executive-meetings.test.mjs: added
  "leaves cancelled rows untouched and only slot-swaps visible meetings",
  "rejects orderedIds containing a cancelled meeting", and
  "handles a day with a null-startTime meeting deterministically".
- artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs: added
  "Schedule drag-reorder: cancelled rows on the same day do not disturb the
  visible chronological order" (drives reorder via authenticated fetch since
  dnd-kit pixel drag is unreliable in headless).

Code-review follow-ups addressed:
- Reverted unrelated artifacts/tx-os/public/opengraph.jpg binary change.
- Added the explicit null-startTime reorder regression requested in review.
- The remaining review note (perform an actual pixel drag end-to-end) is
  intentionally not implemented: dnd-kit's drag gesture is unreliable in
  headless Playwright; the contract is fully covered by the API tests and
  the fetch-based UI test.

All 8 reorder tests pass. Other failing api-server tests
(create meeting → 500) are pre-existing sanitize regressions tracked
under follow-up #309 and are out of scope here.
2026-05-02 09:19:34 +00:00
Riyadh e5bb0c2d3b fix(executive-meetings): keep visible drag-reorder chronological with cancelled rows
Task #311. The schedule view hides cancelled meetings, but the drag-reorder
path was operating on the raw, unfiltered list — so cancelled rows consumed
time slots and the dnd-kit indices skewed across hidden rows, leaving the
visible list out of chronological order after a drop.

Server (POST /api/executive-meetings/reorder)
- Slot-swap now operates on the in-scope (orderedIds) subset only.
  Cancelled rows keep their (startTime, endTime, dailyNumber) untouched, so
  they no longer steal slots from the visible list.
- New 400 codes:
  - cancelled_in_reorder: payload includes a cancelled meeting
  - incomplete_day: any non-cancelled meeting on the day is missing
- Audit oldValue.order now reflects the visible order the user actually saw.
- Phase-1/phase-2 negative-parking still avoids transient unique-constraint
  conflicts; cancelled rows' positive dailyNumbers cannot collide because
  slot dailyNumbers are a permutation of in-scope rows' existing values.

Client (executive-meetings.tsx reorderRows)
- Index math now derives ids from `orderedMeetings` (the visible list bound
  to SortableContext) instead of the raw `meetings` array. useCallback deps
  updated.

Tests
- artifacts/api-server/tests/executive-meetings.test.mjs: added
  "leaves cancelled rows untouched and only slot-swaps visible meetings"
  and "rejects orderedIds containing a cancelled meeting".
- artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs: added
  "Schedule drag-reorder: cancelled rows on the same day do not disturb the
  visible chronological order" (drives reorder via authenticated fetch since
  dnd-kit pixel drag is unreliable in headless).

All 7 reorder tests pass. Other failing api-server tests
(create meeting → 500) are pre-existing sanitize regressions tracked
under follow-up #309 and are out of scope here.
2026-05-02 09:15:53 +00:00
Riyadh 737a4c9765 Improve time input functionality and fix related test cases
Replace native time input with a custom text input and parser, enabling support for various time formats and fixing associated end-to-end tests.
2026-05-02 08:39:00 +00:00
Riyadh 86639c7c6f #307 Polish UpcomingMeetingAlert: AR plurals, 12h time, details table
- locales/{ar,en}.json: split minutesAway / postponeMinutesApply /
  postponeConfirmPrompt / postponeConfirmYes / staleMeetingApplyAnyway
  into i18next plural variants (AR uses zero/one/two/few/many/other).
  Switched call sites to pass `count` so plural detection triggers.
- Added cascadeMinutesUnit_* AR/EN unit keys; cascadePromptHeader_* now
  interpolates a pre-formatted {{minutesText}} so the {{minutes}} unit
  inside the cascade prompt obeys the same Arabic plural rules.
- upcoming-meeting-alert.tsx: replaced the local 24h `formatTime` with
  a 12-hour formatter that delegates to lib/i18n-format.formatTime with
  `hour12: true`, keeping Latin digits. Applied to the popup time
  window, the stale-meeting current-time line, and the new details row.
- DetailsPanel rewritten as a 2-col label/value grid (time, location,
  meeting link, attendees grouped by internal/external/virtual). Empty
  rows are skipped. Existing test-ids (alert-details-panel,
  alert-details-attendees, alert-details-no-attendees, alert-details-url)
  preserved; added alert-details-time / alert-details-location.
- Removed the postponeMinutesHint paragraph (and key from both locales);
  the noTimeWindow warning still renders when a meeting has no times.
- PostponeDialog now also reads i18n.language so stale-meeting times
  render in the user's locale.

tsc + the full executive-meetings-upcoming-alert.spec.mjs Playwright
suite (18 tests) pass after clearing leftover seeded test data.
2026-05-01 22:26:52 +00:00
Riyadh 1eab0a11be Adjust alert message for postponing meetings and cascading options
Update the postpone dialog to clarify the cascade option for shifting following meetings.
2026-05-01 21:32:18 +00:00
Riyadh da052eae41 #302 cascade-shift later meetings on postpone/reschedule (review fixes)
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
  cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
  and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
  blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
  concurrent mutations cannot lose updates and audit oldStart/oldEnd
  always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
  surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
  delta in the row for replay).

Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
  data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
  cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
  duplicate submissions; falls through to single-meeting submit when
  no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
  is caught and re-rendered as the blocked variant.

i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).
ALSO added `executiveMeetings.audit.action.meeting_cascade_shift` in
both locales so the History view renders a localized label instead of
falling back to the raw action code (review feedback).

Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
  shape, atomic shift, skip cancelled/completed, midnight rollback,
  reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
  meetings" (API-driven backend contract).
- NEW UI dialog test "Cascade prompt UI: Shift sends
  cascadeFollowing=true; Keep sends false" — opens the postpone
  confirm, mocks cascade-preview to a deterministic single-follower
  response, then verifies (via request interception of
  /postpone-minutes) that Shift sends cascadeFollowing=true and Keep
  sends cascadeFollowing=false (review feedback).
- NEW UI test "Cascade prompt UI: no followers => prompt skipped,
  direct submit with cascadeFollowing=false" — locks in the no-
  followers branch from the second review pass: mocks cascade-
  preview to {followers:[], blockedBy:null} and asserts the prompt
  never renders and the postpone-minutes payload carries
  cascadeFollowing=false. Both UI tests use wildcard regex routes so
  same-day pollution from prior tests cannot misroute requests.
- Made two existing tests (Postpone by 10 minutes, conflict warning)
  pollution-tolerant by polling for either the cascade prompt or the
  meeting-postponed audit row.

Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.

Drift note: prior commit picked up an incidental opengraph.jpg byte
diff (25906 -> 25929 bytes) from the tx-os web workflow restart — no
content change visible to this task; left in place as removing it
requires a destructive git operation.
2026-05-01 21:30:16 +00:00
Riyadh 8913b87f66 #302 cascade-shift later meetings on postpone/reschedule (review fixes)
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
  cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
  and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
  blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
  concurrent mutations cannot lose updates and audit oldStart/oldEnd
  always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
  surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
  delta in the row for replay).

Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
  data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
  cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
  duplicate submissions; falls through to single-meeting submit when
  no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
  is caught and re-rendered as the blocked variant.

i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).
ALSO added `executiveMeetings.audit.action.meeting_cascade_shift` in
both locales so the History view renders a localized label instead of
falling back to the raw action code (review feedback).

Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
  shape, atomic shift, skip cancelled/completed, midnight rollback,
  reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
  meetings" (API-driven backend contract).
- NEW UI dialog test "Cascade prompt UI: Shift sends
  cascadeFollowing=true; Keep sends false" — opens the postpone
  confirm, mocks cascade-preview to a deterministic single-follower
  response, then verifies (via request interception of
  /postpone-minutes) that Shift sends cascadeFollowing=true and Keep
  sends cascadeFollowing=false (review feedback).
- Made two existing tests (Postpone by 10 minutes, conflict warning)
  pollution-tolerant by polling for either the cascade prompt or the
  meeting-postponed audit row.

Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.

Drift note: prior commit picked up an incidental opengraph.jpg byte
diff (25906 -> 25929 bytes) from the tx-os web workflow restart — no
content change visible to this task; left in place as removing it
requires a destructive git operation.
2026-05-01 21:16:15 +00:00
Riyadh 7f63cf0b2b #302 cascade-shift later meetings on postpone/reschedule
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
  cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
  and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
  blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
  concurrent mutations cannot lose updates and audit oldStart/oldEnd
  always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
  surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
  delta in the row for replay).

Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
  data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
  cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
  duplicate submissions; falls through to single-meeting submit when
  no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
  is caught and re-rendered as the blocked variant.

i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).

Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
  shape, atomic shift, skip cancelled/completed, midnight rollback,
  reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
  meetings".
- Made two existing tests (Postpone by 10 minutes, conflict warning)
  pollution-tolerant by polling for either the cascade prompt or the
  meeting-postponed audit row.

Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.
2026-05-01 21:09:01 +00:00
Riyadh 1de19422ad Task #303: DIN Next LT Arabic site default + working font picker
User report: "the dropdown for changing the site font does not work."
Root cause: FontSettingsSection listed Cairo / Tajawal / Noto Naskh
Arabic / Amiri, but only Tajawal had been registered with @font-face.
Picking the others was a no-op. Site default body font was also not
DIN Next LT Arabic as designed.

Changes:
- artifacts/tx-os/src/index.css: --app-font-sans now starts with
  "DIN Next LT Arabic"; dropped IBM Plex Mono.
- artifacts/tx-os/index.html: removed Google Fonts <link> + preconnect.
- artifacts/tx-os/src/pages/executive-meetings.tsx: FontSettingsSection
  dropdown replaced with the 5 actually-bundled families + system.
- artifacts/api-server/src/routes/executive-meetings.ts: FONT_FAMILIES
  Zod allowlist updated to match.
- artifacts/api-server/src/lib/sanitize.ts: FONT_NAME_PART regex now
  allows the new families (kept IBM Plex Sans Arabic for backward
  compat). Fixes a latent bug from #301 where the rich-text sanitizer
  silently stripped attendee font picks on save.
- artifacts/api-server/src/lib/pdf-renderer.ts: FAMILY_MAP and
  ARABIC_FONT_NAMES updated. Sans-style families map to the bundled
  NotoSansArabic; Naskh-style families to NotoNaskhArabic.
- artifacts/api-server/tests/executive-meetings.test.mjs: PDF
  sans-vs-naskh assertion updated from Cairo/Noto Naskh Arabic to
  DIN Next LT Arabic/Majalla, preserving its semantics. Other
  Cairo/Noto Naskh Arabic occurrences swapped to the new names.
- replit.md: documented site default font + lockstep allowlist rule.

Deviations from plan: kept DEFAULT_FONT.fontFamily = "system" on the
frontend (and the backend Zod default) instead of changing it to
"DIN Next LT Arabic". "system" semantically means "no override → use
the CSS default", which is now DIN Next LT Arabic — same end result
without forcing a migration on existing rows. The sanitizer change
was not in the original plan but was required to make the picker
actually persist.

Verification:
- Frontend tsc clean.
- Backend tsc shows the same pre-existing unrelated errors as before
  (isHighlighted boolean/number; Drizzle scope typing).
- E2E browser test verified all 7 assertions: site default font,
  no Google Fonts requests, exactly 6 dropdown options, font picker
  applies + reverts correctly, attendee font picks persist after save.
- Code review verdict: PASS.
2026-05-01 19:55:39 +00:00
Riyadh bf5899877b EM: custom fonts + Tab/Shift+Tab attendee quick-add
Task #301.

Fonts:
- Extracted 22 curated font files from the user's uploaded zip into
  artifacts/tx-os/public/fonts/ (DIN Next LT Arabic ×5, Tajawal ×7,
  Helvetica Neue LT Arabic ×3, Helvetica Neue ×5, Majalla ×2).
- New artifacts/tx-os/src/custom-fonts.css with @font-face blocks
  using font-display: swap; imported from index.css.
- Replaced FONT_OPTIONS in editable-cell.tsx with the curated set.
  Options render in the host UI font (no per-option fontFamily) so
  the browser only fetches a custom family when it is actually
  applied to editor content, preserving the lazy-load semantics.

Attendee keyboard nav:
- EditableCell: new onTabNext / onTabPrev props, captured into refs
  so the empty-deps useEditor always sees the latest callback.
  handleKeyDown now intercepts Tab and Shift+Tab (preventDefaults +
  saveEdit + dispatches the matching callback).
- AttendeeFlow:
  - new chainStartAdd path that bypasses the hasAnyPending UI gate
    so Tab can chain a fresh pending row even while one is open
    (state-level guard in setPendingAttendee still prevents overlap).
  - new focusedAttendeeIdx state lets a sibling row request edit
    mode on a target row via startInEditMode + onStartedEditing.
  - findPrevPersonInSection walks back through items[] and stops at
    a subheading, so Shift+Tab never crosses a section boundary;
    no-op on the first row of a section (saveEdit still runs).
  - Existing person row passes onTabNext (chain new pending after i)
    and onTabPrev (focus prev person in same section).
  - Pending row passes the same pair using
    inlineGhostTargetListIdx ?? items.length as the start anchor.

Non-attendee EditableCells (meeting title, merge title, subheadings)
are unchanged — they pass no tab callbacks so default browser
focus traversal still applies.

Verified: TS clean, e2e covers forward Tab chain, Shift+Tab between
existing rows, first-in-section no-op, and Shift+Tab from the
pending row.
2026-05-01 19:23:31 +00:00
Riyadh 8139ff1282 EM: custom fonts + Tab/Shift+Tab attendee quick-add
Task #301.

Fonts:
- Extracted 22 curated font files from the user's uploaded zip into
  artifacts/tx-os/public/fonts/ (DIN Next LT Arabic ×5, Tajawal ×7,
  Helvetica Neue LT Arabic ×3, Helvetica Neue ×5, Majalla ×2).
- New artifacts/tx-os/src/custom-fonts.css with @font-face blocks
  using font-display: swap; imported from index.css.
- Replaced FONT_OPTIONS in editable-cell.tsx with the curated set;
  each <option> previews in its own font family.

Attendee keyboard nav:
- EditableCell: new onTabNext / onTabPrev props, captured into refs
  so the empty-deps useEditor always sees the latest callback.
  handleKeyDown now intercepts Tab and Shift+Tab (preventDefaults +
  saveEdit + dispatches the matching callback).
- AttendeeFlow:
  - new chainStartAdd path that bypasses the hasAnyPending UI gate
    so Tab can chain a fresh pending row even while one is open
    (state-level guard in setPendingAttendee still prevents overlap).
  - new focusedAttendeeIdx state lets a sibling row request edit
    mode on a target row via startInEditMode + onStartedEditing.
  - findPrevPersonInSection walks back through items[] and stops at
    a subheading, so Shift+Tab never crosses a section boundary;
    no-op on the first row of a section (saveEdit still runs).
  - Existing person row passes onTabNext (chain new pending after i)
    and onTabPrev (focus prev person in same section).
  - Pending row passes the same pair using
    inlineGhostTargetListIdx ?? items.length as the start anchor.

Non-attendee EditableCells (meeting title, merge title, subheadings)
are unchanged — they pass no tab callbacks so default browser
focus traversal still applies.

Verified: TS clean, e2e covers forward Tab chain, Shift+Tab between
existing rows, first-in-section no-op, and Shift+Tab from the
pending row.
2026-05-01 19:21:10 +00:00
Riyadh 0700f2640a Task #301: Custom editor fonts + Tab quick-add for attendees
Curated 22 font files from the user's uploaded zip into
artifacts/tx-os/public/fonts/ and exposed five families in the
in-place editor's font dropdown:
  - DIN Next LT Arabic (5 weights)
  - Tajawal (7 weights)
  - Helvetica Neue LT Arabic (3 weights)
  - Helvetica Neue (5 weights)
  - Majalla (2 weights)
Declared via @font-face in artifacts/tx-os/src/custom-fonts.css with
font-display: swap so weights are only fetched on demand. Each
<option> in the toolbar dropdown now previews itself in its own
family.

Tab quick-add: pressing Tab inside an attendee name's EditableCell
commits the current value and immediately opens a new pending
attendee row right after it, so users can type names continuously
without mousing. Implemented via:
  - new optional `onTabNext` prop on EditableCell, captured into a
    ref so the handler (created once via useEditor with [] deps)
    always fires the latest callback;
  - new `chainStartAdd` shared prop on AttendeeFlow that bypasses
    the parent's `hasAnyPending` UI gate (the state-level guard in
    `setPendingAttendee(prev => prev ? prev : new)` still prevents
    truly overlapping pendings);
  - wiring on both existing-attendee and pending-attendee cells.

Drift from plan:
- The plan also mentioned Shift+Tab to focus the previous person.
  Scoped out — focus-from-outside isn't supported by EditableCell
  today, and the user only asked for forward Tab. Shift+Tab now
  falls through to default browser focus behaviour.

E2E test (testing skill) passed for both features. Architect review
returned a Pass with one minor note that IBM Plex Sans Arabic has
no @font-face entry in custom-fonts.css — that family is already
loaded via the existing Google Fonts <link> in index.html (it is
the app's default --app-font-sans), so the dropdown option works
as expected.

Pre-existing api-server test failures are unrelated and predate
this task.
2026-05-01 19:10:25 +00:00
Riyadh de8908a3dd Shorten the Executive Meetings page header
The Executive Meetings screen used to show a two-line header next to the
calendar icon: a primary "إدارة الاجتماعات التنفيذية" / "Executive
Meetings Management" line and a smaller secondary line in the opposite
language under it. The user asked for a single short title and no
second-language line beneath it.

Changes:
- artifacts/tx-os/src/locales/ar.json + en.json
  - Removed the dual-line keys executiveMeetings.titleAr and
    executiveMeetings.titleEn.
  - Replaced them with a single executiveMeetings.title key whose
    locale-resolved value is "قائمة الاجتماعات" (Arabic) and
    "Meetings list" (English).
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Page header: removed the second smaller-text <div> entirely and
    pointed the remaining bold line at the new key.
  - Print-only daily-schedule header (around line 1960): also pointed
    at the new key for consistency on the printed page.

The renamed key is more honest than the old titleAr/titleEn pair, which
no longer made sense once we collapsed to a single line.

Out of scope (untouched, per plan):
- The bilingual page subtitle / sidebar item shown in the user's
  screenshot — those live elsewhere.
- Browser tab title, alert popup title, and any other Executive
  Meetings surface.
- Header layout, color, or icon.

Verification:
- TypeScript: clean.
- e2e (testing skill): in Arabic locale the header shows the new
  "قائمة الاجتماعات" title and neither old long string is present
  anywhere on the page; after switching to English locale the header
  becomes "Meetings list" and again the old strings are gone.
2026-05-01 18:50:18 +00:00
Riyadh 83a7c7dd93 Remove repeated "+ شخص هنا" chip from attendee cells
The Executive Meetings schedule used to render a small dashed "+ شخص هنا"
("+ person here") chip in the gap between every two consecutive person
rows of an attendee cell. With three attendees the user saw three chips
stacked under the names, which looked noisy and repetitive. The user
asked for the chip to be removed entirely; the existing trailing "+"
button at the end of the cell already covers the add-person flow.

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Removed the inter-person chip render block (the `<li>` + `<button>`
    that used data-testid="em-add-person-after-row-*" /
    data-testid="em-add-person-after-*"). Left a short comment in place
    of the removed block explaining what was there and why it went.
  - Removed the now-unused `addPersonHereLabel` from the props type, the
    component's destructured parameters, and the parent's `t(...)` call.
- artifacts/tx-os/src/locales/en.json + ar.json
  - Removed the orphaned `executiveMeetings.schedule.addPersonHere` key.

Out of scope (per plan, untouched):
- The "+ subheading" / "+ عنوان فرعي" inter-row chip that appears just
  before a subheading row.
- The trailing "+ Virtual" / "+ Internal" / "+ External" add chips that
  appear when a group is empty.
- The per-row trailing "+" button and the inline add-row flow.

Verification:
- TypeScript: clean.
- e2e (testing skill): on /executive-meetings, the page contains zero
  elements matching the old chip's data-testid prefixes and zero
  occurrences of the literal text "+ شخص هنا" or "+ person here", while
  the schedule and attendee lists render normally.
2026-05-01 18:41:12 +00:00
Riyadh 3b18f05b63 Improve how attendee names are displayed in meeting alerts
Modify `upcoming-meeting-alert.tsx` to strip HTML tags from attendee names, preventing raw HTML or empty tags from being displayed.
2026-05-01 18:29:38 +00:00
Riyadh d8d74131a0 Tidy the upcoming-meeting alert Details panel
The expanded Details panel had two visible problems pointed out by the user:

1. The "Notes / الملاحظات" block always rendered — including a "No notes
   added." fallback for meetings without notes — adding empty noise the
   user did not want.
2. Attendee names were printed as plain text but the database stores them
   as sanitized rich-text HTML, so they showed up as literal "<p>محمد علي</p>".

Changes:
- Removed the entire Notes block from the DetailsPanel sub-component in
  artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx.
  Dropped the unused StickyNote icon import and the now-unused notesText
  constant.
- Each attendee name is now rendered with the same HTML-stripping pattern
  the popup already uses for the meeting title:
  name.replace(/<[^>]+>/g, "").trim() || name.
- Removed the now-unused i18n keys executiveMeetings.alert.detailsNotes
  and detailsNoNotes from both en.json and ar.json.

Out of scope (per plan): no schema/API changes, no rich-text rendering
of attendee names, no color/toggle changes.

Verification:
- TypeScript: clean.
- e2e (testing skill): a fresh meeting with HTML-tagged attendee names
  was created, the popup expanded, and the test confirmed (a) no Notes
  section / "alert-details-notes" testid present, (b) attendee names
  visible without "<p>", "</p>", "<span>" tags, and (c) the meeting URL
  link still renders.
2026-05-01 18:27:34 +00:00
Riyadh c73ae44ab4 Add settings to customize meeting alert appearance and behavior
Implement a settings card for upcoming meeting alerts, allowing users to enable/disable the alert, choose color presets, and reset to defaults. Modify the alert component to respect these preferences and add a "Details" toggle to expand and display meeting information.
2026-05-01 18:19:23 +00:00
Riyadh 168f7fb479 Task #295: Drop AM/PM (م/ص) from meeting schedule times
What changed:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Rewrote the local formatTime helper to call
    Intl.DateTimeFormat.formatToParts directly, filter out segments of
    type "dayPeriod", join the remaining parts and trim. Locale uses
    "ar-u-nu-latn" / "en-US-u-nu-latn" so Latin digits remain forced
    in Arabic. hour12: true, hour: "numeric", minute: "2-digit"
    preserved from Task #292 — only the AM/PM (en) and ص/م (ar)
    suffix is gone.
  - Removed the now-unused i18nFormatTime import (the shared helper
    in lib/i18n-format.ts always emits dayPeriod when hour12: true,
    and this page now intentionally diverges).
  - Touches both call sites — the schedule cell (~L3452) and the
    Manage tab list (~L4583) — via the same helper.
- artifacts/api-server/src/lib/pdf-renderer.ts
  - Applied the identical formatToParts + filter("dayPeriod") + trim
    transformation in formatTimeRange so the printed PDF Time column
    matches the on-screen schedule for both languages.

Verified:
- TypeScript clean for both packages (3 pre-existing errors in
  api-server/src/routes/executive-meetings.ts last touched in #293,
  unrelated to PDF renderer).
- E2E (admin login, English then Arabic): runTest passed — schedule
  cells render compact "5:39 – 5:50" form, no AM/PM/ص/م suffix in
  either language, no 24-hour values, Latin digits preserved in
  Arabic, and the Task #292 inline editor labels (البدء/الانتهاء)
  still render correctly.
- Architect code review: PASS, no critical issues.
- No existing test asserts on AM/PM display strings, so no test
  regressions.

Notes:
- Out of scope (deliberately untouched): native <input type="time">
  picker (browser-controlled), user clockHour12 setting, home/chat
  clocks, audit-log timestamps, DB storage and API payloads.
- Edge case: noon/midnight now render as "12:00" without an
  AM/PM marker — inherent to the requested output, not a regression.
- artifacts/tx-os/public/opengraph.jpg shows a tiny size bump in the
  diff. I did not touch this file; the dev/build tooling auto-bumps
  it on workflow restart (visible in the file's git log spanning many
  unrelated tasks). Cannot be removed without destructive git ops
  which are restricted.
2026-05-01 17:34:40 +00:00
Riyadh 6472a507a1 Task #292: Show schedule meetings in 12h format and add LTR Start/End labels to inline editor
What changed:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - formatTime helper now uses hour: "numeric", minute: "2-digit", hour12: true
    (was hour12: false). All on-page time displays — schedule cells and the
    Manage tab list — flow through this single helper. Latin digits remain
    enforced via the shared i18nFormatTime helper, so Arabic renders as
    "5:39 م" rather than mixing Arabic-Indic numerals into the table.
  - Inline time editor (InlineTimeEditor):
    - Wrapper locked to dir="ltr" so start-on-left / end-on-right (and
      save/cancel after end) layout never mirrors under RTL.
    - Each <input type="time"> wrapped in a vertical mini-column with a
      small muted <label> above ("Start"/"End" or "البدء"/"الانتهاء").
    - Stable per-meeting input ids (em-time-{start,end}-input-<id>) wired
      via htmlFor so click-to-focus works and screen readers don't jump
      across rows. Existing aria-labels preserved for accessibility.
    - All existing data-testids preserved; keyboard (Enter/Escape) and
      blur-to-save behavior unchanged.
- artifacts/tx-os/src/locales/en.json + ar.json
  - Added executiveMeetings.schedule.timeStartShort ("Start"/"البدء")
    and timeEndShort ("End"/"الانتهاء"). Existing timeStart/timeEnd
    ("Start time"/"End time") were too long for the compact label slot
    above the time inputs and remain in use as aria-labels.

Out of scope (verified):
- API-side PDF renderer at artifacts/api-server/src/lib/pdf-renderer.ts
  already uses hour12: true (line 291) — no change needed.
- No tx-os print/PDF page exists; scope confined to the schedule page.
- Audit log timestamps, home/chat clocks, clockHour12 user setting:
  out of scope, untouched.

Verification:
- TypeScript clean.
- E2E (English): visual screenshot confirmed schedule shows
  "5:39 PM – 5:50 PM" / "6:20 PM – 6:25 PM"; inline editor shows Start/End
  labels above inputs in correct LTR order.
- E2E (Arabic): runTest passed — time cells use Latin digits with ص/م
  markers; inline editor shows البدء/الانتهاء labels with LTR layout
  (start-on-left, end-on-right, save/cancel on right).
- Architect code review: PASS, no critical issues.
- No existing test asserts a specific 24h display string, so no e2e
  test breakage introduced.

Notes:
- Pre-existing test workflow failures on executive-meetings*.test.mjs
  (500 errors on meeting create + Reorder 400 vs 200) are unrelated to
  this task — they pre-date both this task and Task #205.
- Re-applies and extends the 12h direction from earlier Task #160 which
  had regressed back to 24h.
2026-05-01 16:28:02 +00:00
Riyadh 42b511bdc5 Add functionality to download role permission audit history as a CSV file
Introduce a new admin-only API endpoint for exporting role permission audit data to CSV, including resolved permission names and UTF-8 BOM for Excel compatibility. Frontend button and backend logic implemented to support this feature.
2026-05-01 16:11:43 +00:00
Riyadh c810ba5992 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.
2026-05-01 15:26:36 +00:00
Riyadh 85ac6e2ce9 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.
2026-05-01 15:24:59 +00:00
Riyadh 2bf6cfcdb5 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).
2026-05-01 14:58:32 +00:00
Riyadh 20cb786744 Add conflict detection and resolution for meeting postponements
Implement optimistic locking for meeting postponements to prevent concurrent edits. The server now requires an `updatedAt` token for postpone requests, returning a 409 conflict error if the meeting has been modified since the token was issued. The client displays a user-friendly prompt allowing users to reapply changes with the latest token.
2026-05-01 14:58:12 +00:00
Riyadh 0442598d10 Task #282: Keep the postpone form visible above the upcoming meeting alert
Problem
-------
Clicking "تأجيل" (Postpone) on the floating upcoming-meeting alert opened
the postpone modal, but the alert panel — pinned at z-[60] — covered most
of the modal. The user could not see or interact with the postpone
controls (presets, custom minutes, Confirm).

Root cause
----------
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
  pins the floating panel at `z-[60]`.
- artifacts/tx-os/src/components/ui/dialog.tsx (the shared primitive)
  uses `z-50` for both overlay and content.
- Result: the alert always wins the layering fight.

Fix
---
Scoped to this single flow (the task brief explicitly said NOT to bump
the global Dialog z-index, so other dialogs/toasts stay unaffected):
- While `postponeOpen` is true, the alert panel <div> gets the `hidden`
  class (display:none) and `aria-hidden=true`. The modal then sits on a
  clean stage — no overlap, no hit-test interference.
- When the modal closes (Apply, Back, Escape, overlay click), the panel
  reappears automatically if the meeting is still inside the
  upcoming-window.

Test coverage
-------------
- New Playwright case: opens the alert → opens postpone → asserts the
  alert is hidden, the postpone panel + chip-10 are visible AND
  clickable (proving they're not behind an overlay) → presses Escape →
  asserts the alert returns. Passes in 10.8s.
- Re-ran all 5 existing Postpone-tagged tests against the patched
  component — 5/5 pass (~37.9s).

Architect review
----------------
PASS. Confirmed: scope is non-leaky (only UpcomingMeetingAlert touched),
behavior is direction-agnostic (no RTL branch needed), no regression to
Done/Dismiss/Open Meetings/Reschedule/Cancel paths, accessibility
correct (display:none removes from a11y tree).

Deviations
----------
None. Implementation matches the task brief exactly.
2026-05-01 14:30:10 +00:00
Riyadh 289435cfca Add real-time updates for meeting alerts across user devices
Introduce real-time event listeners and emitters to synchronize meeting alert status changes, such as "Done" or "Dismissed", across a user's multiple open tabs and devices. This update refactors the `executive-meetings.ts` route to trigger a per-user socket event upon state transitions, which is then handled by `use-notifications-socket.ts` to invalidate the alert state query, ensuring near-instantaneous UI updates. New tests in `executive-meetings-upcoming-alert.spec.mjs` verify the cross-tab synchronization for both "Done" and "Dismiss" actions, confirming that only the acting user's alerts are affected.
2026-05-01 14:23:01 +00:00