Commit Graph

134 Commits

Author SHA1 Message Date
riyadhafraa 8b3c15ba21 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

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

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

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

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

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

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

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

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

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

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

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

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

Out of scope (pre-existing, not introduced here):
- 3 failing tests in executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts.
2026-05-12 12:31:43 +00:00
riyadhafraa 9a8dc3175f Task #512: Per-recipient Delete in Notes Inbox
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.

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

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

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

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

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

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

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

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

executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
2026-05-12 10:51:31 +00:00
riyadhafraa d119480684 Task #499: fix Quick Actions dialog X overlap + iPad long-press drag feedback
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).

1) Quick Actions dialog header overlap
   - Radix DialogContent renders the Close (X) at physical
     `right-4 top-4` regardless of `dir`. In AR (RTL) the
     `text-right`-aligned title "إجراءات سريعة" pinned to the same
     edge and visibly overlapped the X.
   - Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
     the title's right edge always clears the close button. Works
     for AR (overlap eliminated) and EN (just adds trailing slack).

2) iPad long-press drag feedback
   - dnd-kit's TouchSensor has a 200ms activation delay; previously
     the row gave no cue during the hold window so users lifted
     early thinking the schedule didn't register.
   - Fix: added `isPressing` state toggled via capture-phase
     onPointerDown/Up/Cancel handlers on the row tr, gated on
     `pointerType === "touch"` && `effectiveDragEnabled`. Capture
     phase chosen so handlers don't collide with safeRowDragListeners
     (which spreads dnd-kit's bubble-phase onPointerDown).
   - Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
     0.45)` ring composed AFTER quickOpen/selection/current rings so
     it never outranks a real selection state. Per spec, the cue
     appears during the 200ms hold AND continues throughout the
     active drag (we deliberately do NOT clear isPressing on the
     rising edge of isDragging). Two defensive useEffects clear
     `isPressing`: one when drag becomes disabled (capability flip /
     edit-mode toggle), one tied to the FALLING edge of isDragging
     (drag just ended) as a belt-and-suspenders guard against stale
     visual state when dnd-kit absorbs the touch sequence and our
     row's own pointerup never fires.
   - data-pressing="true" attribute exposed on tr for tests.
   - TouchSensor delay (200ms) and tolerance (8px) unchanged.

Tests added:
 - executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
   close button + title bounding boxes don't overlap in AR + EN.
 - executive-meetings-row-touch-press-feedback.spec.mjs: uses
   hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
   data-pressing flips true on touchstart, clears on touchend, and
   tap-to-open quick-actions dialog still works for short taps;
   second test verifies the cue persists past the 200ms window.

Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
2026-05-11 15:43:30 +00:00
riyadhafraa 9b0dbb037c Task #499: fix Quick Actions dialog X overlap + iPad long-press drag feedback
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).

1) Quick Actions dialog header overlap
   - Radix DialogContent renders the Close (X) at physical
     `right-4 top-4` regardless of `dir`. In AR (RTL) the
     `text-right`-aligned title "إجراءات سريعة" pinned to the same
     edge and visibly overlapped the X.
   - Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
     the title's right edge always clears the close button. Works
     for AR (overlap eliminated) and EN (just adds trailing slack).

2) iPad long-press drag feedback
   - dnd-kit's TouchSensor has a 200ms activation delay; previously
     the row gave no cue during the hold window so users lifted
     early thinking the schedule didn't register.
   - Fix: added `isPressing` state toggled via capture-phase
     onPointerDown/Up/Cancel handlers on the row tr, gated on
     `pointerType === "touch"` && `effectiveDragEnabled`. Capture
     phase chosen so handlers don't collide with safeRowDragListeners
     (which spreads dnd-kit's bubble-phase onPointerDown).
   - Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
     0.45)` ring composed AFTER quickOpen/selection/current rings so
     it never outranks a real selection state. Two defensive
     useEffects clear `isPressing`: one when drag becomes disabled
     (capability flip / edit-mode toggle), one tied to the
     `isDragging` lifecycle so dnd-kit-managed release paths (drop
     outside the row, sensor-managed cancel) can't leave a stale
     ring behind.
   - data-pressing="true" attribute exposed on tr for tests.
   - TouchSensor delay (200ms) and tolerance (8px) unchanged.

Tests added:
 - executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
   close button + title bounding boxes don't overlap in AR + EN.
 - executive-meetings-row-touch-press-feedback.spec.mjs: uses
   hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
   data-pressing flips true on touchstart, clears on touchend, and
   tap-to-open quick-actions dialog still works for short taps;
   second test verifies the cue persists past the 200ms window.

Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
2026-05-11 15:40:48 +00:00
riyadhafraa 27c5cc381c Task #499: fix Quick Actions dialog X overlap + iPad long-press drag feedback
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).

1) Quick Actions dialog header overlap
   - Radix DialogContent renders the Close (X) at physical
     `right-4 top-4` regardless of `dir`. In AR (RTL) the
     `text-right`-aligned title "إجراءات سريعة" pinned to the same
     edge and visibly overlapped the X.
   - Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
     the title's right edge always clears the close button. Works
     for AR (overlap eliminated) and EN (just adds trailing slack).

2) iPad long-press drag feedback
   - dnd-kit's TouchSensor has a 200ms activation delay; previously
     the row gave no cue during the hold window so users lifted
     early thinking the schedule didn't register.
   - Fix: added `isPressing` state toggled via capture-phase
     onPointerDown/Up/Cancel handlers on the row tr, gated on
     `pointerType === "touch"` && `effectiveDragEnabled`. Capture
     phase chosen so handlers don't collide with safeRowDragListeners
     (which spreads dnd-kit's bubble-phase onPointerDown).
   - Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
     0.45)` ring composed AFTER quickOpen/selection/current rings so
     it never outranks a real selection state. Defensive useEffect
     clears `isPressing` if drag becomes disabled mid-press.
   - data-pressing="true" attribute exposed on tr for tests.
   - TouchSensor delay (200ms) and tolerance (8px) unchanged.

Tests added:
 - executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
   close button + title bounding boxes don't overlap in AR + EN.
 - executive-meetings-row-touch-press-feedback.spec.mjs: uses
   hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
   data-pressing flips true on touchstart, clears on touchend, and
   tap-to-open quick-actions dialog still works for short taps;
   second test verifies the cue persists past the 200ms window.

Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
2026-05-11 15:38:17 +00:00
riyadhafraa b5b55f89fb Task #497: Allow row-drag rotation without time windows
Lifted the per-row missing-time drag block (originally #492). Meetings
without a (startTime, endTime) tuple are a normal state and now rotate
freely alongside timed rows.

Server (artifacts/api-server/src/routes/executive-meetings.ts):
- Dropped the `no_time_window` early-return guard in
  /api/executive-meetings/rotate-content.
- Sort + slot construction now tolerate null start times (NULLS LAST,
  tie-break by id) so a null tuple rotates as a real slot.
- All other safeguards (cancelled_in_rotate, optimistic lock,
  renumberDayByStartTime, audit logging) remain intact.

Client (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Removed missingTimeCount / dayRotatable memos, the dayRotatable
  prop wiring, dragBlockedByMissingTime + effectiveDragEnabled
  composition, the row's data-drag-blocked / native title /
  cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire
  DragBlockedTooltipButton component + its render site, and the
  no_time_window errorToast branch in rotateContent. Pruned now-dead
  Tooltip + AlertTriangle imports.

i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block
(tooltip + errorToast) from en.json and ar.json.

Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs`
and added `executive-meetings-rotate-allows-missing-times.spec.mjs`,
which inserts 3 meetings (one with null times), verifies no
drag-blocked warning button / aria-disabled, drags the top row to the
bottom slot, asserts the rotate-content POST succeeds, and confirms
exactly one row still has a null tuple after rotation.

Verified: row-drag, row-quick-actions, and the new spec all pass
(8/8). Architect code review PASSED.
2026-05-11 15:23:33 +00:00
riyadhafraa 59d838b8f2 Task #497: Allow row-drag rotation without time windows
Lifted the per-row missing-time drag block (originally #492). Meetings
without a (startTime, endTime) tuple are a normal state and now rotate
freely alongside timed rows.

Server (artifacts/api-server/src/routes/executive-meetings.ts):
- Dropped the `no_time_window` early-return guard in
  /api/executive-meetings/rotate-content.
- Sort + slot construction now tolerate null start times (NULLS LAST,
  tie-break by id) so a null tuple rotates as a real slot.
- All other safeguards (cancelled_in_rotate, optimistic lock,
  renumberDayByStartTime, audit logging) remain intact.

Client (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Removed missingTimeCount / dayRotatable memos, the dayRotatable
  prop wiring, dragBlockedByMissingTime + effectiveDragEnabled
  composition, the row's data-drag-blocked / native title /
  cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire
  DragBlockedTooltipButton component + its render site, and the
  no_time_window errorToast branch in rotateContent. Pruned now-dead
  Tooltip + AlertTriangle imports.

i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block
(tooltip + errorToast) from en.json and ar.json.

Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs`
and added `executive-meetings-rotate-allows-missing-times.spec.mjs`,
which inserts 3 meetings (one with null times), verifies no
drag-blocked warning button / aria-disabled, drags the top row to the
bottom slot, asserts the rotate-content POST succeeds, and confirms
exactly one row still has a null tuple after rotation.

Verified: row-drag, row-quick-actions, and the new spec all pass
(8/8). Architect code review PASSED.
2026-05-11 15:18:06 +00:00
riyadhafraa 34dd8d50a4 Task #496: hide row Quick Actions in edit mode + drop missing-time banner
- ScheduleSection: gate `quickActionsCanMutate` on `canMutate && !editMode`
  so clicking a row in edit mode no longer opens the Postpone dialog.
  In view mode the quick-actions surface still works as before.
- MeetingRow: add a useEffect that force-closes local `quickOpen` when
  capability flips off (edit-mode toggle while dialog is open) and gate
  `quickOpenShadow` + `data-quick-open` on capability so no stale frame
  paints. Caught by code review.
- Remove the amber `em-rotate-blocked-hint` missing-time banner per
  product feedback. Kept `missingTimeCount` / `dayRotatable` so row
  drag still aborts client-side and the per-row tooltip + native
  title still explain why.
- Drop now-unused i18n keys `executiveMeetings.rotate.needsTimeWindow.hint*`
  from en.json and ar.json (zero/one/two/few/many/other variants).
  `tooltip` + `errorToast` strings stay (consumed by tooltip button
  + rotate-content catch handler).
- Update executive-meetings-rotate-needs-time-window.spec.mjs to drop
  the banner visibility assertion and the post-unblock `toHaveCount(0)`
  re-check. Tooltip / aria-disabled / no-rotate-POST / successful-
  rotate-after-unblock checks remain.

Verified: rotate-needs-time-window + row-quick-actions specs (7 tests)
all pass with --workers=1.
2026-05-11 14:57:13 +00:00
riyadhafraa 8fd16eb2dc Block EM row-drag when any meeting on the day lacks a time window (#492)
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast that
confused AR users and didn't tell them what to fix.

UI changes:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
  in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow gates useSortable + safeRowDragListeners on
  `effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
  blocked specifically by missing time, the <tr> shows
  `cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
  `data-drag-blocked="no_time_window"`, and a bilingual native `title`
  (desktop hover fallback). The aria-disabled is spread AFTER dnd-kit's
  attributes so it wins the prop merge.
- New `DragBlockedTooltipButton` rendered inside each blocked row's #
  cell — a Radix Tooltip-wrapped warning icon that opens on hover,
  focus, AND tap (manual open toggle) so iPad users can read the
  explanation (native title long-press is unreliable on touch).
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
  surfaces the missing-time count using i18next's CLDR plural resolver
  (`t(key, { count })`) so AR picks the correct
  zero/one/two/few/many/other form and EN picks one/other.
- rotateContent's catch handler detects ApiError code "no_time_window"
  (local apiJson now attaches .code/.status to thrown Errors) and
  shows the bilingual `executiveMeetings.rotate.needsTimeWindow.errorToast`
  instead of the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
  in en.json + ar.json (tooltip, hint plurals, errorToast). AR has
  full zero/one/two/few/many/other variants.
- ScheduleSection's `t` prop type widened to accept i18next options.

Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs:
  inserts one untimed + two timed meetings, asserts the hint banner +
  every row's aria-disabled + data-drag-blocked, asserts the
  Tooltip-wrapped info button is visible with the correct localized
  aria-label + tooltip body (substring match — Radix renders sr-only
  duplicate), attempts a drag and asserts NO rotate-content POST is
  sent and DB state is unchanged. Then UPDATE-s the missing time and
  asserts: hint disappears, aria-disabled lifts on every row, info
  button is gone, drag now succeeds (rotate-content POST + DB
  start_time mutation).
- Existing executive-meetings-row-drag, touch-reorder, and
  row-quick-actions specs still pass.

Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
       artifacts/tx-os/src/locales/{en,ar}.json,
       artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
2026-05-11 14:35:19 +00:00
riyadhafraa e19506aac7 Block EM row-drag when any meeting on the day lacks a time window (#492)
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast ("Every
meeting must have a scheduled time window to rotate") that confused AR
users and didn't tell them what to fix.

Fix:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
  in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow now gates useSortable + safeRowDragListeners on
  `effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
  blocked specifically by missing time, the <tr> shows
  `cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
  `data-drag-blocked="no_time_window"`, and a bilingual `title`
  tooltip. The aria-disabled is spread AFTER dnd-kit's attributes so
  it wins the prop merge.
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
  surfaces the missing-time count so mutators see why drag is paused.
- rotateContent's catch handler now detects ApiError code
  "no_time_window" (local apiJson now attaches .code/.status to the
  thrown Error) and shows the bilingual
  `executiveMeetings.rotate.needsTimeWindow.errorToast` instead of
  the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
  in en.json + ar.json (tooltip, hint_one/_other [+ AR plurals],
  errorToast).

Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs
  inserts one untimed + two timed meetings and asserts the hint
  banner + every row's aria-disabled + data-drag-blocked, then
  attempts a drag and asserts NO rotate-content POST is sent and
  DB state is unchanged. After UPDATE-ing the missing time, asserts
  the hint and aria-disabled lift.
- Existing executive-meetings-row-drag and touch-reorder specs still
  pass — drag works exactly as before on fully-timed days.

Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
       artifacts/tx-os/src/locales/{en,ar}.json,
       artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
2026-05-11 14:21:02 +00:00
riyadhafraa 38a55b8b42 Task #491: center quick-actions surface + frame the selected row
The user reported that the small quick-actions popover (which holds
the Postpone trigger) was hard to find — it floated next to the
clicked row and they sometimes lost track of which meeting they
were about to act on. They asked for the surface to appear in the
center of the screen, and for the row they clicked to be clearly
framed in a color while the surface is open.

Changes:
- Replaced the row's row-anchored Popover (PopoverAnchor +
  PopoverContent) with a centered Dialog (DialogContent) that
  Radix renders into a portal. The Dialog uses the existing
  `executiveMeetings.quickActions.label` translation key for its
  title (already present in both Arabic and English) and reuses
  #490's polished Postpone Button unchanged. Test ID stays
  `em-row-quick-${id}` so existing e2e specs continue to target
  the same surface.
- Added a third inset-shadow ring (`inset 0 0 0 3px
  ${highlightColor}`) to the row's composed boxShadow stack while
  the row's quickOpen state is true. Listed first in the stack so
  it becomes the outermost frame and wins visually over the
  existing current-meeting / bulk-select rings. Uses the
  page-level highlightPrefs.color so the frame matches the user's
  chosen accent. Also exposes `data-quick-open="true"` on the
  <tr> for tests.
- Updated the row-quick-actions e2e: the postpone-end-to-end test
  now asserts `role="dialog"` on the surface and the row carries
  `data-quick-open` while open and loses it after Postpone is
  clicked. Added a separate test for Escape closing the surface
  and clearing the row frame.

Verified: 5/5 quick-actions tests pass, plus both drag specs
(row-drag + iPad touch-reorder) still pass.
2026-05-11 13:28:55 +00:00
riyadhafraa c7942a221f Task #491: center quick-actions surface + frame the selected row
The user reported that the small quick-actions popover (which holds
the Postpone trigger) was hard to find — it floated next to the
clicked row and they sometimes lost track of which meeting they
were about to act on. They asked for the surface to appear in the
center of the screen, and for the row they clicked to be clearly
framed in a color while the surface is open.

Changes:
- Replaced the row's row-anchored Popover (PopoverAnchor +
  PopoverContent) with a centered Dialog (DialogContent) that
  Radix renders into a portal. The Dialog uses the existing
  `executiveMeetings.quickActions.label` translation key for its
  title (already present in both Arabic and English) and reuses
  #490's polished Postpone Button unchanged. Test ID stays
  `em-row-quick-${id}` so existing e2e specs continue to target
  the same surface.
- Added a third inset-shadow ring (`inset 0 0 0 3px
  ${highlightColor}`) to the row's composed boxShadow stack while
  the row's quickOpen state is true. Listed first in the stack so
  it becomes the outermost frame and wins visually over the
  existing current-meeting / bulk-select rings. Uses the
  page-level highlightPrefs.color so the frame matches the user's
  chosen accent. Also exposes `data-quick-open="true"` on the
  <tr> for tests.
- Updated the row-quick-actions e2e: the postpone-end-to-end test
  now asserts `role="dialog"` on the surface and the row carries
  `data-quick-open` while open and loses it after Postpone is
  clicked. Added a separate test for Escape closing the surface
  and clearing the row frame.

Verified: 5/5 quick-actions tests pass, plus both drag specs
(row-drag + iPad touch-reorder) still pass.
2026-05-11 13:25:21 +00:00
riyadhafraa f1c9ef71b7 Task #491: center quick-actions surface + frame the selected row
The user reported that the small quick-actions popover (which holds
the Postpone trigger) was hard to find — it floated next to the
clicked row and they sometimes lost track of which meeting they
were about to act on. They asked for the surface to appear in the
center of the screen, and for the row they clicked to be clearly
framed in a color while the surface is open.

Changes:
- Replaced the row's row-anchored Popover (PopoverAnchor +
  PopoverContent) with a centered Dialog (DialogContent) that
  Radix renders into a portal. The Dialog uses the existing
  `executiveMeetings.quickActions.label` translation key for its
  title (already present in both Arabic and English) and reuses
  #490's polished Postpone Button unchanged. Test ID stays
  `em-row-quick-${id}` so existing e2e specs continue to target
  the same surface.
- Added a third inset-shadow ring (`inset 0 0 0 3px
  ${highlightColor}`) to the row's composed boxShadow stack while
  the row's quickOpen state is true. Listed first in the stack so
  it becomes the outermost frame and wins visually over the
  existing current-meeting / bulk-select rings. Uses the
  page-level highlightPrefs.color so the frame matches the user's
  chosen accent. Also exposes `data-quick-open="true"` on the
  <tr> for tests.
- Updated the row-quick-actions e2e: the postpone-end-to-end test
  now asserts `role="dialog"` on the surface and the row carries
  `data-quick-open` while open and loses it after Postpone is
  clicked. Added a separate test for Escape closing the surface
  and clearing the row frame.

Verified: 5/5 quick-actions tests pass, plus both drag specs
(row-drag + iPad touch-reorder) still pass.
2026-05-11 13:22:58 +00:00
riyadhafraa bcda13d9c8 Ensure postpone button styling and text are correct for all devices
Add assertions to verify the styling classes and text content of the postpone button in the executive meetings row quick actions spec.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: bf387ee6-aedd-4092-acfb-d6a42a1969f2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/591TuZw
Replit-Helium-Checkpoint-Created: true
2026-05-11 13:03:39 +00:00
riyadhafraa c7168a959b Task #490: iPad row drag + Postpone button polish
The user reported that on iPad they could no longer drag a meeting
row up or down, and that the Postpone button in the row quick-actions
popover looked plain.

Changes:
- Sensor split for the schedule row DnD: replaced PointerSensor with
  MouseSensor (mouse only, distance:6) + kept TouchSensor (touch,
  delay:200ms / tolerance:8px). On iOS Safari, PointerSensor was
  receiving touch-derived pointer events and competing with the OS
  scroll claim — the browser reclaimed the gesture as a vertical
  scroll before the finger moved 6px, so drag never activated.
  Splitting sensors lets each input modality use the activation
  constraint that actually works for it. PointerSensor remains
  imported because the attendee-reorder dialog still uses it
  (out of scope).
- Polished the Postpone button in the row quick-actions popover:
  swapped the bespoke <button> for the project's <Button> component
  (variant=default, size=sm), gave it a 40px tap target (h-10),
  shadow-sm, gap-2, min-w-[11rem], and a flex-row-reverse flip in
  RTL so the icon stays adjacent to the label in Arabic.
- Rewrote tests/executive-meetings-touch-reorder.spec.mjs which was
  stale — it referenced the removed grip and the retired /reorder
  endpoint. New spec runs under iPad emulation (hasTouch + isMobile,
  768x1024), clears the persisted edit-mode pref + asserts edit mode
  reads OFF (so the "drag works without edit mode" regression is
  explicitly guarded), long-presses the # cell, drags down, and
  asserts /api/executive-meetings/rotate-content fires + DB rotation
  matches expectation.

Tests: touch-reorder (iPad TouchSensor), row-drag (desktop mouse via
MouseSensor), and popover (4/4) all pass. Backend unchanged.
2026-05-11 12:58:17 +00:00
riyadhafraa f4f94981f8 Task #489 fix: decouple row drag from edit mode
Previous review rejected because row drag was gated on
`effectiveCanMutate = canMutate && editMode`, forcing users to
enter Edit mode to drag. Spec requires drag to work in view mode
for any user with mutate permission.

Changes:
- MeetingRow: new `dragEnabled` prop separate from `canMutate`.
  Used by useSortable.disabled, safeRowDragListeners gate,
  attributes spread, and cursor-grab class.
- Parent passes `dragEnabled={canMutate}` (raw, not gated by
  editMode); `canMutate={effectiveCanMutate}` retained for inline
  cell editing.
- handleRowClick: scope interactive `closest()` to descendants
  (interactive !== e.currentTarget). Required because the row now
  carries dnd-kit's role="button" in view mode and an unscoped
  closest() matched the row itself, swallowing every popover click.
- e2e: removed edit-toggle click from row-drag spec — drag now
  passes in view mode.

Tests: row-drag spec (view mode), popover spec (4/4), schedule-
features drag tests (290, 834), backend rotate-content (6/6) all
pass. 3 unrelated schedule-features failures reference a missing
em-nav-settings testid that does not exist in source — pre-existing
broken tests, not caused by this task.
2026-05-11 12:28:46 +00:00
riyadhafraa f53f7307da Task #489: row-wide drag rotates meeting content; time + daily numbers anchored
Backend
- New POST /api/executive-meetings/rotate-content (zod-validated) rotates
  ONLY meeting content through fixed (start_time, end_time, daily_number)
  slots. Same-date enforced; per-meeting expectedUpdatedAt → 409 stale;
  incomplete day (missing visible row) → 400.
- ExecutiveMeetingsRotateContentBody added in lib/api-zod (manual.ts).
- 6 backend tests cover happy path, stale, different_dates, 401, 403,
  incomplete_day. Existing /swap-times tests still pass.

Frontend (artifacts/tx-os)
- Whole <tr> is now the drag handle (the dedicated GripVertical button is
  retired). useSortable is gated on canMutate; safeRowDragListeners
  filters drags whose target is an interactive descendant (button, input,
  edit/time cells, row-actions, bulk-select). useSortable `attributes`
  are spread only when canMutate so view-mode rows stay clickable
  (otherwise aria-disabled blocked the popover trigger).
- onRowDragEnd → rotateContent(fromId, toId): optimistic patch reassigns
  each chronological slot's tuple to the new occupant; rolls back + toast
  on failure.
- Quick-actions popover now contains only Postpone (#486 Move up/down
  buttons removed).

Tests (artifacts/tx-os)
- New tests/executive-meetings-row-drag.spec.mjs: drags Alpha → Charlie
  position by # cell, asserts rotate-content fires and slots stay
  anchored.
- tests/executive-meetings-row-quick-actions.spec.mjs: drops up/down
  cases, keeps Postpone + skip-surfaces + viewer.
- tests/executive-meetings-schedule-features.spec.mjs: two legacy grip
  drag tests rewritten to drag the row body and target /rotate-content
  (the legacy /reorder route + tests are intentionally untouched).

Drift / notes
- Architect flagged a medium-severity hardening note: rotate-content
  FOR UPDATE locks orderedIds but not the day-scope completeness query.
  Out of #489 scope; no follow-up created (proposeFollowUpTasks was
  already consumed on #486).
2026-05-11 12:14:08 +00:00
riyadhafraa 936e184b05 #486: Executive Meetings row click → quick-actions popover
Clicking any meeting row on the Executive Meetings schedule (gated only
on canMutate, not editMode) opens a small popover with Move up / Move
down / Postpone. Move up/down swap only the (startTime, endTime) tuple
between the clicked meeting and its chronological neighbour on the same
date — the Time column stays visually anchored to its row position.

Backend
- POST /executive-meetings/swap-times: transactional swap with FOR
  UPDATE row locking, optimistic-lock conflict shape (stale_meeting +
  conflict payload), date/time-window guards, audit logging, and
  renumberDayByStartTime + day-changed broadcast.
- Zod schema in lib/api-zod/src/manual.ts.

Frontend
- Shared lib/api-json.ts JSON helper.
- ScheduleSection.swapTimes does an optimistic (startTime, endTime)
  swap against the day query cache and rolls back on failure (mirrors
  the existing inline-edit UX).
- Edge enablement (canQuickMoveUp/Down) now derived from the full
  orderedMeetings index, not the search-filtered displayedMeetings, so
  "Move up/down" disabled state always matches the true day boundary
  (and never silently swaps with a hidden filtered neighbour).
- MeetingRow uses Popover/PopoverAnchor with skip rules: ARIA roles
  (button/checkbox/switch/combobox/dialog) and em-time-* / em-edit-* /
  em-row-grip / em-row-actions data-testid prefixes do NOT open the
  popover.
- PostponeDialog reused from upcoming-meeting-alert.tsx.

Tests
- Backend swap-times: happy path, stale_meeting (409), different_dates
  (400), no_time_window (400), unauth (401), viewer-no-mutate (403),
  malformed-timestamp (400) — all 7 pass.
- Hardened expectedUpdatedAt zod schema to z.string().datetime() so
  malformed tokens fail at validation with a controlled 400 instead of
  bubbling up as a 500.
- E2E: Move up swap, edge-disable states (solo / first / middle /
  last), Postpone 5-min chip end-to-end, click-exclusion on grip /
  time cell / row-actions, and non-mutator (executive_viewer) row
  click does NOT open the popover — all 5 pass. Each test uses its own
  future date to avoid cross-test pollution.

Code review approved on second pass. Pre-existing failures in other
suites (executive-meetings reorder, font-settings, notes-share,
service-orders) are unrelated to this task and predate it.

Follow-ups proposed: #487 (keyboard a11y on the popover), #488 (edit-
mode test gaps).
2026-05-11 11:13:19 +00:00
riyadhafraa 02b3b374b0 #486: Executive Meetings row click → quick-actions popover
Clicking any meeting row on the Executive Meetings schedule (gated only
on canMutate, not editMode) opens a small popover with Move up / Move
down / Postpone. Move up/down swap only the (startTime, endTime) tuple
between the clicked meeting and its chronological neighbour on the same
date — the Time column stays visually anchored to its row position.

Backend
- POST /executive-meetings/swap-times: transactional swap with FOR
  UPDATE row locking, optimistic-lock conflict shape (stale_meeting +
  conflict payload), date/time-window guards, audit logging, and
  renumberDayByStartTime + day-changed broadcast.
- Zod schema in lib/api-zod/src/manual.ts.

Frontend
- Shared lib/api-json.ts JSON helper.
- ScheduleSection.swapTimes does an optimistic (startTime, endTime)
  swap against the day query cache and rolls back on failure (mirrors
  the existing inline-edit UX).
- MeetingRow uses Popover/PopoverAnchor with skip rules: ARIA roles
  (button/checkbox/switch/combobox/dialog) and em-time-* / em-edit-* /
  em-row-grip / em-row-actions data-testid prefixes do NOT open the
  popover.
- PostponeDialog reused from upcoming-meeting-alert.tsx.

Tests
- Backend swap-times: happy path, stale_meeting (409), different_dates
  (400), no_time_window (400), unauth (401), viewer-no-mutate (403) —
  all 6 pass.
- E2E: Move up swap, edge-disable states (solo / first / middle /
  last), Postpone 5-min chip end-to-end, click-exclusion on grip /
  time cell / row-actions — all 4 pass. Each test uses its own future
  date to avoid cross-test pollution.

Code review approved on second pass. Pre-existing failures in other
suites (executive-meetings reorder, font-settings, notes-share,
service-orders) are unrelated to this task and predate it.

Follow-ups proposed: #487 (keyboard a11y on the popover), #488 (edit-
mode test gaps).
2026-05-11 11:03:24 +00:00
riyadhafraa b1b77395d0 #486 Executive Meetings: row-click quick actions popover (Move up / Move down / Postpone)
Clicking any meeting row on the schedule now opens a small popover with three
quick actions: Move up, Move down, and Postpone. Gated only on canMutate (NOT
editMode) per spec.

Backend
- New POST /executive-meetings/swap-times endpoint (artifacts/api-server/src/
  routes/executive-meetings.ts). FOR UPDATE locks both rows by ascending id to
  avoid deadlocks, optimistic-lock check via expectedUpdatedAt{A,B} (returns
  409 stale_meeting + conflict.lastActor — same shape PostponeDialog
  understands), guards different_dates and no_time_window, swaps only
  (startTime, endTime), audits each row as `meeting_swap_times`, calls
  renumberDayByStartTime so the # column matches the new chronological order,
  and broadcasts emitExecutiveMeetingsDayChanged.
- New zod body schema ExecutiveMeetingsSwapTimesBody in lib/api-zod/src/manual.ts.

Frontend
- New shared lib/api-json.ts (ApiError + apiJson) extracted from upcoming-
  meeting-alert.tsx so the page can reuse the same fetch/error contract.
- upcoming-meeting-alert.tsx exports PostponeDialog so the page can mount it
  for quick-action postpone without duplicating UI.
- artifacts/tx-os/src/pages/executive-meetings.tsx: Schedule wires
  swapTimes/quickMoveUp/quickMoveDown/postpone state, computes per-day
  neighbours via meetingNumbersById, and mounts a single page-level
  PostponeDialog. MeetingRow wraps `<tr>` in a Popover/PopoverAnchor; row
  onClick opens the popover with skip rules for buttons/inputs/contenteditable
  and ARIA roles (button/checkbox/switch/combobox/dialog) plus testid
  prefixes (em-row-grip/-actions/-select, em-edit-*, em-merge-edit-*, em-time-*)
  so the time cell's inline editor and other affordances don't collide.
- en/ar locales gain executiveMeetings.quickActions.{label,moveUp,moveDown,postpone}.

Tests
- artifacts/api-server/tests/executive-meetings-swap-times.test.mjs (new):
  happy path, 409 stale_meeting (with conflict actor), 400 different_dates,
  400 no_time_window. Each scenario uses a distinct far-future date to avoid
  daily_number races with seeded data.
- artifacts/tx-os/tests/executive-meetings-row-quick-actions.spec.mjs (new):
  drives the date input, verifies row click → popover, Move up swap reflected
  in DB, and Postpone item opens the dialog.

Code review (architect) flagged an edit-mode conflict where the time cell
(role=button div) would also bubble to the row handler; fixed by adding ARIA
role and em-time-* skip rules. Architect also flagged keyboard-trigger gap
and edit-mode test gaps — proposed as follow-ups.

Other test failures in the repo (executive-meetings reorder, font-settings,
notes-share, service-orders) pre-date this change and are unrelated.
2026-05-11 10:55:34 +00:00
riyadhafraa 2d131fddf0 #484 Reset meetings edit mode on leaving the app
The Executive Meetings page persisted its global "Edit / View" toggle
in localStorage (key `em-schedule-edit-mode-v1:<userId>`). Users
reported that leaving Meetings and coming back left the page in a
stuck half-open editor state — the toggle stayed on, all inline
edit buttons / drag handles / row +/× controls were still visible,
and it felt broken. Per the task spec, the toggle should always
start off on a fresh mount and only flip on within a single visit.

Source change (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Removed the `EDIT_MODE_STORAGE_KEY` constant (replaced with a
  short comment explaining the non-persistence decision).
- Dropped the `editModeStorageKey` per-user namespaced memo.
- Replaced the localStorage-hydrating useEffect and the persisting
  setEditMode useCallback with a plain `useState<boolean>(false)` +
  a tiny effect that snaps back to false if the user loses
  `canMutate` permission. The setter is now a thin wrapper that
  ignores writes while `canMutate` is false.
- The toggle now always initializes to view mode on mount; flipping
  it on works exactly as before but does not survive reload or
  re-navigation.

Test changes (artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs):
- Renamed the existing "Edit toggle hides editing affordances..." test
  to "...and resets to view mode on reload / re-navigation (#484)".
- Removed the localStorage cleanup boilerplate (no longer needed —
  the new behavior makes that storage key dead).
- Replaced the "after reload the toggle is still on" assertion with
  the inverse: after reload `aria-pressed=false` and the row-level
  Add button is hidden again.
- Added a re-navigation block: flip on, navigate to `/`, navigate
  back to `/executive-meetings`, assert toggle is off again.
- Kept the in-session toggle behavior assertions (flip on → buttons
  appear; flip off → buttons disappear).

Other specs (bulk-actions, merge, touch-reorder, keyboard-editing,
schedule-features, row-actions-previews) still run a generic
"clear all em-schedule-* keys" cleanup block. Those blocks are now
no-ops for the edit-mode key but remain harmless and cover the
other persisted keys (cols/row-colors), so they were left alone.

Verified: `tsc --noEmit` clean; the new "Edit toggle hides ... and
resets to view mode on reload / re-navigation (#484)" test passes.

Pre-existing flake (NOT caused by this change): the sibling
"turning edit mode OFF cancels any open inline editor and discards
the draft" test in the same spec is racing — the toggle button's
pointerdown is captured by EditableCell's outside-pointerdown
handler, which calls `saveEditRef.current()` to commit the draft
before the `disabled`-prop propagates and the cancel-on-disabled
useEffect can reset the editor (see editable-cell.tsx ~lines
295-307 vs ~421-426). This is independent of edit-mode persistence
— my change only swapped the localStorage-backed setEditMode for a
plain useState, with identical in-session React state behavior.
Fixing the EditableCell race is out of scope for #484.

Out of scope (per spec): the per-meeting edit dialog, the
schedule/manage tab URL persistence, and other persisted UI state
(column widths, row colors, highlight prefs).
2026-05-11 10:02:36 +00:00
riyadhafraa f8b0969cd2 Improve responsiveness for meeting reschedule dialog
Adjust grid breakpoints for the postpone dialog and update RTL test assertions.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 370d4507-cc98-4781-a35f-85ce37566751
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/591TuZw
Replit-Helium-Checkpoint-Created: true
2026-05-11 08:12:08 +00:00
riyadhafraa 1f225298b1 #485 Fix overlapping layout in the Postpone meeting dialog
In Arabic/RTL the Postpone dialog (opened from the upcoming-meeting
alert) was visibly overlapping its own borders on two tabs:
1. Postpone tab → cascade preview ("الاجتماعات المتأثرة"): the times
   column ran wider than the pink panel, the title column pushed it
   off-screen, and the meeting-# header wrapped to two lines while
   the others stayed on one.
2. Reschedule tab: three native date / start / end inputs forced into
   md:grid-cols-3 inside an sm:max-w-md dialog, which clipped the
   right-most input against the dialog frame in RTL.

Both issues were pure layout — the dialog was simply too narrow for
its content.

Changes (artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx):
- Bumped the postpone DialogContent from `sm:max-w-md` (≈448 px) to
  `sm:max-w-2xl` (≈672 px). Still full width on phones, capped on
  desktop. Reschedule's existing `grid-cols-1 sm:grid-cols-2
  md:grid-cols-3` now has room to actually render three inputs at md
  without clipping (Input already defaults to w-full).
- CascadePromptBlock affected-meetings table: switched to
  `table-fixed` with an explicit `<colgroup>` (w-20 number / flex
  title / w-40 times). The number column was bumped from w-12 to
  w-20 so the Arabic header "رقم الاجتماع" + `whitespace-nowrap`
  has room at common font sizes (raised during code review). Title
  cell keeps its `truncate` + `title=` tooltip but no longer needs
  `max-w-[180px]`. Added `whitespace-nowrap` to # and times headers
  so the latter never wraps. Wrapper got `overflow-x-hidden` to
  belt-and-suspenders the no-horizontal-scroll guarantee. Action
  button row already used `flex-wrap` — preserved.

Tests (artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs):
- Added a Reschedule-tab regression test that opens the dialog at
  1280×800 in Arabic and asserts the bounding boxes of
  `reschedule-date` / `reschedule-start` / `reschedule-end` all sit
  inside the dialog's bounding box (1 px tolerance).
- Added a Cascade-table regression test (raised during code review)
  that seeds a primary + 2 followers, postpones to fire the cascade
  prompt, and asserts the `cascade-followers-list` and the last
  `<td>` (times) of the first follower row both sit inside the
  dialog at desktop width in RTL.
- Confirmed no behavior regressions across the postpone-by-10,
  cascade follower display, tab-switching, and reschedule-to-tomorrow
  tests.

Out of scope (per task spec): backend cascade/postpone logic, the
floating alert panel itself, dialog visual restyling, and the Cancel
tab beyond what comes "for free" with the wider dialog.

The pre-existing `test` workflow failures (executive-meetings reorder,
font-settings roundtrip, notes-share recipient PATCH 403/404) are
unrelated to this layout change and were already failing before.
2026-05-11 08:08:54 +00:00
riyadhafraa 21bac11aa5 #485 Fix overlapping layout in the Postpone meeting dialog
In Arabic/RTL the Postpone dialog (opened from the upcoming-meeting
alert) was visibly overlapping its own borders on two tabs:
1. Postpone tab → cascade preview ("الاجتماعات المتأثرة"): the times
   column ran wider than the pink panel, the title column pushed it
   off-screen, and the meeting-# header wrapped to two lines while
   the others stayed on one.
2. Reschedule tab: three native date / start / end inputs forced into
   md:grid-cols-3 inside an sm:max-w-md dialog, which clipped the
   right-most input against the dialog frame in RTL.

Both issues were pure layout — the dialog was simply too narrow for
its content.

Changes (artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx):
- Bumped the postpone DialogContent from `sm:max-w-md` (≈448 px) to
  `sm:max-w-2xl` (≈672 px). Still full width on phones, capped on
  desktop. Reschedule's existing `grid-cols-1 sm:grid-cols-2
  md:grid-cols-3` now has room to actually render three inputs at md
  without clipping (Input already defaults to w-full).
- CascadePromptBlock affected-meetings table: switched to
  `table-fixed` with an explicit `<colgroup>` (w-12 number / flex
  title / w-40 times). Title cell keeps its `truncate` + `title=`
  tooltip but no longer needs `max-w-[180px]`. Added `whitespace-nowrap`
  to # and times headers so the latter never wraps. Wrapper got
  `overflow-x-hidden` to belt-and-suspenders the no-horizontal-scroll
  guarantee. Action button row already used `flex-wrap` — preserved.

Tests (artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs):
- Added a regression test that opens the dialog at 1280×800 in
  Arabic, switches to the Reschedule tab, and asserts the bounding
  boxes of `reschedule-date` / `reschedule-start` / `reschedule-end`
  all sit inside the dialog's bounding box (1 px tolerance).
- Re-ran the full upcoming-alert spec to confirm no behavior
  regressions in postpone-by-minutes, cascade prompts, cancel,
  reschedule-to-tomorrow, dismiss, or the alert position-clamp test.

Out of scope (per task spec): backend cascade/postpone logic, the
floating alert panel itself, dialog visual restyling, and the Cancel
tab beyond what comes "for free" with the wider dialog.

The pre-existing `test` workflow failures (executive-meetings reorder,
font-settings roundtrip, notes-share recipient PATCH 403/404) are
unrelated to this layout change and were already failing before.
2026-05-11 08:05:51 +00:00
riyadhafraa 2b31b5e3aa Remove title field from notes and update tests to reflect changes
Refactors the notes feature by removing the `title` field and updating all related tests and UI components to use `content` instead.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7464969c-726a-4ec2-a3b2-8ff63f91f6ed
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/591TuZw
Replit-Helium-Checkpoint-Created: true
2026-05-11 07:53:40 +00:00
riyadhafraa d6b19748f7 Update upcoming meeting alert to better display cascade information
Refactor upcoming meeting alert component to update the cascade prompt table structure and adjust end-to-end tests to reflect the changes in column count and content assertions.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9509e0a2-e6ad-4b70-8d04-56b729a8f67c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/8BABrKh
Replit-Helium-Checkpoint-Created: true
2026-05-10 16:09:15 +00:00
riyadhafraa 7ea7744e26 #481 Polish cascade-affected meetings table in postpone/reschedule prompt
- Drop the leading "#" index column; dailyNumber already serves as a
  stable per-row identifier.
- Replace the hard-coded amber background/border with the user's chosen
  alert accent via hexToRgba(accent, 0.12 / 0.45). Threaded `accent`
  through PostponeDialog → CascadePromptBlock so children don't read
  prefs directly. Both the loading and main prompt blocks track the
  accent; the rose blocked-by-midnight variant is intentionally kept.
- Render times as localized 12-hour with ص/م (AR) or AM/PM (EN) by
  reusing the shared formatTime helper instead of slicing "HH:mm".
- Drop now-unused i18n key cascadeColIndex from ar.json + en.json.
- Update the e2e test to match the 3-column schema (meeting#, title,
  times); kept the dailyNumber assertion in the first cell.

tsc clean. Cascade specs pass (Postpone by 10, Reschedule cascade,
Cascade prompt UI: Shift/Keep, no-followers fallthrough).
2026-05-10 16:07:20 +00:00
riyadhafraa 5e37d392c0 #479: Render upcoming-alert attendees as numbered tables per group
- DetailsPanel in upcoming-meeting-alert.tsx: replaced the per-group
  `<ul class="list-disc">` with a real `<table class="w-full">`. Each
  attendee row is `<tr>` with two `<td>`s: a narrow tabular-nums index
  cell (`{idx + 1}.`, scope="row") and the cleaned name cell. The
  group heading is rendered as `<caption>` (text-start, font-bold,
  text-xs) so it spans both columns and is announced as the table
  caption to AT. Each table also carries `aria-label={group.heading}`.
- Numbering restarts at 1 per group (each group is its own table); a
  code comment notes the one-line tweak to use a continuous count.
- Preserved: `data-testid="alert-details-attendees"` wrapper,
  `max-h-40 overflow-y-auto` scroll, empty-state, location/URL rows,
  and the section header total ("الحضور (n)"). RTL/LTR work via
  `text-start` and `pe-1`.
- e2e: added a new test in
  `artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs`
  that seeds a meeting with two subheading-delimited groups (3
  external + 1 internal), expands details, and asserts two tables
  render with rows starting at "1." in each group.

Verification:
- `pnpm exec tsc --noEmit` clean.
- New attendees-table spec passes (1/1, 16.5s).

No server, schema, or i18n key changes; Tailwind utilities only.
2026-05-10 15:44:23 +00:00
riyadhafraa ae31ed90c6 #480 Show cascade-affected meetings as a numbered table
Replace the simple <ul> in the cascade-prompt panel inside the upcoming-
meeting alert with a 4-column table: row index, the meeting's
dailyNumber, title (truncate + title=), and "from → to". The new
`meetingNumbersById` map is built once in UpcomingMeetingAlert from
the already-loaded dayData and threaded through PostponeDialog into
both CascadePromptBlock call sites (postpone-minutes and reschedule).
Existing `cascade-followers-list` and `cascade-follower-<id>` testids
are preserved on the table/rows so prior selectors keep matching.

Adds 4 new i18n keys (cascadeColIndex/MeetingNumber/Title/Times) in
ar.json and en.json. The legacy cascadeListItem key is left in place
since it isn't worth a separate cleanup.

Extends the existing "Cascade prompt UI: Shift sends cascadeFollowing"
e2e test to assert the first row's index cell renders "1" and the
meeting-number cell renders the seeded follower's dailyNumber.
insertImminentMeeting now also returns dailyNumber so the assertion
can read it. tsc clean; targeted cascade specs pass (3/3).
2026-05-10 15:40:17 +00:00
riyadhafraa 26ed7a195e notes(sent): multi-select + bulk delete on Sent tab (task #472)
- Add useBulkDeleteSentNotes hook with bounded concurrency (worker
  pool, cap = 6) over the existing per-id DELETE /notes/:id endpoint;
  returns {ok, failed} so the UI can render a precise toast. One cache
  invalidation at the end (notes + folders).
- notes.tsx: page-level selection state for the Sent tab only
  (sentSelectionMode, selectedSentIds, bulkDeleteOpen). Header
  Select/Cancel toggle, sticky bulk-action bar with count, select-all/
  clear, in-bar Cancel, Delete, and an AlertDialog confirmation. Auto
  exits selection mode when leaving the Sent tab; prunes selected ids
  to currently visible filteredSent on every change (search, refresh,
  successful delete).
- SentList: switched the outer card from a nested <button> to a
  div role="button" tabIndex=0 with Enter/Space handler so the
  selection-mode Checkbox is no longer a nested interactive control.
  Checkbox is aria-hidden / pointer-events-none inside selection mode.
- i18n: notes.bulk* keys (Select / Cancel / SelectAll / Clear / Count
  with _one plural / Delete / Confirm title+body / Result success /
  partial / failure) in both en.json and ar.json.
- New e2e: tests/notes-sent-bulk-delete.spec.mjs seeds 3 sent notes,
  selects 2, confirms the bulk delete, asserts the cards are gone and
  the surviving note remains (DB + UI).
- Out of scope (proposed as follow-ups #473/#474/#475): undo on bulk
  delete, multi-select on Inbox/Archived tabs, additional e2e cases.
2026-05-10 14:35:45 +00:00
riyadhafraa ef1f23a11d Task #466: Clean notes page from email-like 4-tab bar
User asked to remove the prominent 4-tab bar (My Notes / Inbox / Sent /
Archive) from the notes page because it made the page feel like an email
client, and they rarely use the share-between-users feature.

Changes (artifacts/tx-os/src/pages/notes.tsx):
- Deleted the inline-flex TabButton row that held the four view tabs.
- Page now opens directly on the unified "active" feed.
- Added an overflow DropdownMenu (⋯ MoreVertical icon) on the end of the
  controls row containing three items: Inbox (with unread badge), Sent,
  Archived. Each item calls setView() with the same TabId values, so all
  downstream branching (data fetching, filtering, composer visibility,
  folder rail) is unchanged.
- Unread inbox badge now appears (a) as a small numeric dot on the
  overflow trigger when not currently in Inbox view, and (b) inside the
  dropdown next to the Inbox item — the existing
  `data-testid="notes-inbox-unread-badge"` is preserved.
- Added a small "← My Notes" back button (testid notes-back-to-active)
  that appears only when view !== "active" so the user can return after
  drilling into Inbox/Sent/Archived without a visible tab bar.
- Removed the now-unused TabButton component definition.

Tests (artifacts/tx-os/tests/notes-inbox.spec.mjs):
- Replaced the two notes-tab-received / notes-tab-sent clicks with the
  open-dropdown-then-select-item sequence using the new overflow testids.

Validation:
- pnpm tsc --noEmit passes for @workspace/tx-os.
- Architect review: no severe issues; layout, a11y, RTL, z-index, and
  state-reset behavior all noted as sound.
- The `test` workflow has pre-existing failures (executive-meetings PDF,
  notes-share, groups-crud) that are explicitly out of scope per the
  task description and tracked by other open follow-up tasks.

No backend, schema, API, or i18n JSON changes were needed: the file
uses inline t() defaults, and `notes.tabs.received/sent/archived` keys
are reused inside the dropdown.
2026-05-10 13:49:53 +00:00
riyadhafraa daa4f6c038 Task #463: @dnd-kit notes drag + reorder
- Replace HTML5+touch drag with @dnd-kit (PointerSensor distance:8,
  TouchSensor delay:200/tol:8) matching home.tsx pattern.
- Add sort_order column to notes; ORDER BY asc(sortOrder), updatedAt desc.
- New PATCH /notes/reorder endpoint: strict isPinned boolean validation,
  bucket+permission scoped, all writes in db.transaction for atomicity.
- PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change.
- Client useReorderNotes hook with optimistic cache update.
- handleDragEnd builds reorder payload from FULL bucket (owner notes or
  shared-folder bucket via ref), not the filtered/search subset, so
  hidden siblings retain stable order.
- SharedFolderView publishes its data.notes via bucketRef when viewer
  has edit permission, enabling correct reorder in shared folders.
- Layout fix at narrow viewport: rail stacks above notes
  (flex-col md:flex-row) so iPad portrait drag has proper bbox.
- Playwright tests: notes-folders.spec.mjs both desktop pointer drag
  and touch long-press drag pass (32s).
- OpenAPI codegen skipped: notes-api.ts is hand-written.
- Out of scope (pre-existing failures): executive-meetings reorder/font,
  notes-share PATCH 403/404, groups-crud rollback.
2026-05-10 13:10:01 +00:00
riyadhafraa 28ddbe33d1 Task #441: Fix notification sound on iPad/Safari (Web Audio migration)
- Rewrote artifacts/tx-os/src/lib/notification-sounds.ts to use the
  Web Audio API (lazy AudioContext + decoded AudioBuffer cache) instead
  of HTMLAudioElement. unlock() resumes the ctx for any non-running
  state (handles iOS `interrupted`), plays a 1-sample silent buffer to
  satisfy iOS Safari's gesture requirement, and pre-warms decodes.
  decodeAudioData wrapped to support both Promise and callback forms
  for older WebKit.
- play() dispatches an AUTOPLAY_BLOCKED_EVENT with `{ isIos }` detail
  when ctx isn't running. Throttle is 3 s; new bypassThrottle option
  (used by testPlay) skips the gate AND avoids advancing lastPlayAt
  so settings previews can't suppress real socket chimes.
- use-audio-unlock.ts keeps gesture listeners attached and re-calls
  unlock() on every gesture (handles iPad ctx auto-suspend on
  background / AirPlay).
- use-autoplay-hint.ts reads detail.isIos and switches between
  notifSettings.autoplayHint and notifSettings.autoplayHintIos
  (added to en.json + ar.json with Control Center / silent-switch
  guidance).
- notification-settings.tsx per-sound preview button now calls
  notificationPlayer.testPlay(id).
- Test globals __txosNotifPlayCount/__txosNotifLastSound preserved;
  added __txosNotifCtxState() accessor (dev/test-only, gated by
  import.meta.env.MODE).
- Added tests/notification-sound-webaudio.spec.mjs covering the
  preview button gesture path: counter increments, throttle bypass
  works, singleton AudioContext reaches `running` state.

Notes:
- Pre-existing api-server tsc errors in executive-meetings.ts and the
  failing `test` workflow are unrelated to this task and out of scope.
2026-05-07 14:15:53 +00:00
riyadhafraa 093f42665c Task #438: collaborative checklists + meeting alert animation parity
Backend
- New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any
  active (non-archived) recipient can flip an item's done flag.
- Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live
  note row so concurrent toggles from multiple collaborators can't
  lose each other's updates. Live notes.items + every
  note_recipients.items snapshot are mirrored atomically in the same
  tx.
- Emits note_checklist_changed to the audience minus the actor with
  the full updated items array so receivers can patch state without
  an extra fetch.

Frontend
- useToggleChecklistItem mutation hook with optimistic updates across
  notes/sent/received/thread caches and rollback on error.
- Incoming-note popup checklist is now interactive with a local
  override for instant visual feedback (popup renders from socket
  payload, not from the query cache).
- Thread checklist toggles are gated by effective permission (owner,
  admin, or non-archived recipient) to mirror server auth.
- Socket handler for note_checklist_changed invalidates relevant
  query keys AND patches the open popup payload directly via a new
  IncomingNotePopupContext.updateChecklistItems action so
  collaborators' open popups stay in sync.

Meeting alert animation parity (upcoming-meeting-alert.tsx)
- Added scale-95 -> scale-100 + fade entrance with the same spring
  cubic-bezier the note popup uses, retriggered per eligible meeting.
- Replaced the 1px border with a ring-4 colored frame driven by
  alertPrefs.accent (via boxShadow so the color is dynamic).
- Added an animate-ping accent halo around the drag-handle icon to
  match the popup's avatar pulse.

Tests
- artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests:
  owner/recipient toggle + mirroring, 403 non-recipient, 403
  archived, 400 non-checklist, 404 unknown item, 400 invalid body).
- artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e:
  recipient ticks an item from the popup, server + sender snapshot
  both reflect the change).

Code review (architect) findings addressed:
- (critical) lost-update race -> tx with row lock.
- (critical) non-atomic snapshot mirror -> same tx.
- (critical) open popup didn't re-render on socket fanout ->
  updateChecklistItems context action.
- UI permission parity for archived recipients -> thread gates
  onToggle.
2026-05-07 11:42:38 +00:00
riyadhafraa 3d8840afee Task #438: collaborative checklists + meeting alert animation parity
Backend
- New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any
  active (non-archived) recipient can flip an item's done flag.
- Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live
  note row so concurrent toggles from multiple collaborators can't
  lose each other's updates. Live notes.items + every
  note_recipients.items snapshot are mirrored atomically in the same
  tx.
- Emits note_checklist_changed to the audience minus the actor with
  the full updated items array so receivers can patch state without
  an extra fetch.

Frontend
- useToggleChecklistItem mutation hook with optimistic updates across
  notes/sent/received/thread caches and rollback on error.
- Incoming-note popup checklist is now interactive with a local
  override for instant visual feedback (popup renders from socket
  payload, not from the query cache).
- Thread checklist toggles are gated by effective permission (owner,
  admin, or non-archived recipient) to mirror server auth.
- Socket handler for note_checklist_changed invalidates relevant
  query keys AND patches the open popup payload directly via a new
  IncomingNotePopupContext.updateChecklistItems action so
  collaborators' open popups stay in sync.

Meeting alert animation parity (upcoming-meeting-alert.tsx)
- Added scale-95 -> scale-100 + fade entrance with the same spring
  cubic-bezier the note popup uses, retriggered per eligible meeting.
- Replaced the 1px border with a ring-4 colored frame driven by
  alertPrefs.accent (via boxShadow so the color is dynamic).
- Added an animate-ping accent halo around the drag-handle icon to
  match the popup's avatar pulse.

Tests
- artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests:
  owner/recipient toggle + mirroring, 403 non-recipient, 403
  archived, 400 non-checklist, 404 unknown item, 400 invalid body).
- artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e:
  recipient ticks an item from the popup, server + sender snapshot
  both reflect the change).

Code review (architect) findings addressed:
- (critical) lost-update race -> tx with row lock.
- (critical) non-atomic snapshot mirror -> same tx.
- (critical) open popup didn't re-render on socket fanout ->
  updateChecklistItems context action.
- UI permission parity for archived recipients -> thread gates
  onToggle.
2026-05-07 11:37:05 +00:00
riyadhafraa 6292d52ffa Task #438: collaborative checklists + meeting alert animation parity
Backend
- New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any
  active (non-archived) recipient can flip an item's done flag.
- Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live
  note row so concurrent toggles from multiple collaborators can't
  lose each other's updates. Live notes.items + every
  note_recipients.items snapshot are mirrored atomically in the same
  tx.
- Emits note_checklist_changed to the audience minus the actor with
  the full updated items array so receivers can patch state without
  an extra fetch.

Frontend
- useToggleChecklistItem mutation hook with optimistic updates across
  notes/sent/received/thread caches and rollback on error.
- Incoming-note popup checklist is now interactive with a local
  override for instant visual feedback (popup renders from socket
  payload, not from the query cache).
- Thread checklist toggles are gated by effective permission (owner,
  admin, or non-archived recipient) to mirror server auth.
- Socket handler for note_checklist_changed invalidates relevant
  query keys AND patches the open popup payload directly via a new
  IncomingNotePopupContext.updateChecklistItems action so
  collaborators' open popups stay in sync.

Meeting alert animation parity (upcoming-meeting-alert.tsx)
- Added scale-95 -> scale-100 + fade entrance with the same spring
  cubic-bezier the note popup uses, retriggered per eligible meeting.
- Replaced the 1px border with a ring-4 colored frame driven by
  alertPrefs.accent (via boxShadow so the color is dynamic).
- Added an animate-ping accent halo around the drag-handle icon to
  match the popup's avatar pulse.

Tests
- artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests:
  owner/recipient toggle + mirroring, 403 non-recipient, 403
  archived, 400 non-checklist, 404 unknown item, 400 invalid body).
- artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e:
  recipient ticks an item from the popup, server + sender snapshot
  both reflect the change).

Code review (architect) findings addressed:
- (critical) lost-update race -> tx with row lock.
- (critical) non-atomic snapshot mirror -> same tx.
- (critical) open popup didn't re-render on socket fanout ->
  updateChecklistItems context action.
- UI permission parity for archived recipients -> thread gates
  onToggle.
2026-05-07 11:34:55 +00:00
riyadhafraa a430812998 Task #438: collaborative checklists + meeting alert animation parity
Backend
- New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any
  active (non-archived) recipient can flip an item's done flag.
- Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live
  note row so concurrent toggles from multiple collaborators can't
  lose each other's updates. Live notes.items + every
  note_recipients.items snapshot are mirrored atomically in the same
  tx.
- Emits note_checklist_changed to the audience minus the actor with
  the full updated items array so receivers can patch state without
  an extra fetch.

Frontend
- useToggleChecklistItem mutation hook with optimistic updates across
  notes/sent/received/thread caches and rollback on error.
- Incoming-note popup checklist is now interactive with a local
  override for instant visual feedback (popup renders from socket
  payload, not from the query cache).
- Thread checklist toggles are gated by effective permission (owner,
  admin, or non-archived recipient) to mirror server auth.
- Socket handler for note_checklist_changed invalidates relevant
  query keys AND patches the open popup payload directly via a new
  IncomingNotePopupContext.updateChecklistItems action so
  collaborators' open popups stay in sync.

Meeting alert animation parity (upcoming-meeting-alert.tsx)
- Added scale-95 -> scale-100 + fade entrance with the same spring
  cubic-bezier the note popup uses, retriggered per eligible meeting.
- Replaced the 1px border with a ring-4 colored frame driven by
  alertPrefs.accent (via boxShadow so the color is dynamic).
- Added an animate-ping accent halo around the drag-handle icon to
  match the popup's avatar pulse.

Tests
- artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests:
  owner/recipient toggle + mirroring, 403 non-recipient, 403
  archived, 400 non-checklist, 404 unknown item, 400 invalid body).
- artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e:
  recipient ticks an item from the popup, server + sender snapshot
  both reflect the change).

Code review (architect) findings addressed:
- (critical) lost-update race -> tx with row lock.
- (critical) non-atomic snapshot mirror -> same tx.
- (critical) open popup didn't re-render on socket fanout ->
  updateChecklistItems context action.
- UI permission parity for archived recipients -> thread gates
  onToggle.
2026-05-07 11:24:57 +00:00
riyadhafraa 213895e9e8 Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

Added a new e2e ("recipient popup renders checklist items for a
checklist note") that composes a checklist note via the UI, sends it,
and asserts `incoming-note-popup-checklist` is visible with both
items — locks in the noteKind/items socket plumbing.

All affected e2e tests pass (notes-popup-on-receive note + reply +
checklist, notes-popup-touch-tap both, notes-inbox); queue unit
tests pass.
2026-05-07 09:29:31 +00:00
riyadhafraa ff3c567b3c Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

All affected e2e tests pass (notes-popup-on-receive note + reply,
notes-popup-touch-tap both, notes-inbox); queue unit tests pass.
2026-05-07 09:23:29 +00:00
riyadhafraa fc14ca4c01 Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup`
rendered a `fixed inset-0 z-[110]` full-viewport wrapper. Stacking
that layer on top of the upcoming-meeting alert plus a Radix toast
intermittently caused the next finger tap to be swallowed — the user
saw the popup "hang". A simultaneous re-render storm from
synchronous query invalidations on multi-event socket fanouts made
it worse on slower hardware.

Fixes:
- Drop the full-viewport wrapper: render the popup card directly
  as a sized `fixed` element. No more invisible viewport-sized
  layer between touch and the rest of the UI.
- Suppress the redundant note/reply toast when the floating
  popup actually accepts the event. The popup IS the surface;
  doubling up just stacks one more floating layer.
- Remove `autoFocus` on the Reply button — focus-steal was
  contributing to the perceived hang.
- Coalesce `react-query` `invalidateQueries` calls in the
  notifications socket: collect into a Set and flush once per
  animation frame so a burst of socket events triggers a single
  refetch pass instead of N synchronous re-renders. Cleaned up
  on socket disconnect.

Tests (artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs):
- "recipient on a touch device can tap Reply on the floating
  note popup" — verifies the popup is no longer viewport-sized,
  the duplicate toast is suppressed, and a real `locator.tap()`
  succeeds.
- "popup + meeting alert simultaneously: tap on popup still
  works" — the user's exact scenario: seeds an imminent meeting
  so the alert appears, triggers a real cross-user note
  delivery so both float simultaneously, then taps Reply on
  the popup and confirms it dismisses while the alert remains.

Both tests + tsc --noEmit clean.

Files:
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
- artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new)
2026-05-06 11:54:13 +00:00
riyadhafraa e283a83162 Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup`
rendered a `fixed inset-0 z-[110]` full-viewport wrapper with
`pointer-events-none`. Stacking that layer on top of the upcoming
meeting alert plus a Radix toast intermittently caused the next
finger tap to be swallowed — the user saw the popup "hang".

Fixes:
- Drop the full-viewport wrapper: render the popup card directly
  as a sized `fixed` element. No more invisible viewport-sized
  layer between touch and the rest of the UI.
- Suppress the redundant note/reply toast when the floating
  popup actually accepts the event. The popup IS the surface;
  doubling up just stacks one more floating layer.
- Remove `autoFocus` on the Reply button. A new popup arriving
  while the user is mid-tap would steal focus, contributing to
  the perceived hang.

Tests:
- New `notes-popup-touch-tap.spec.mjs`: emulates a touch viewport,
  sends a real cross-user note, verifies popup bounding box is
  card-sized (not viewport-sized), the redundant toast is
  suppressed, and a `locator.tap()` on Reply dismisses the
  popup and navigates.
- `tsc --noEmit` clean.

Files:
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
- artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new)
2026-05-06 11:43:23 +00:00
riyadhafraa 32db433581 notes: show note-card actions on touch devices (task #425)
Original task: on phones and iPad the per-card action row (color,
label, send, archive, delete) was hidden by `opacity-0
group-hover:opacity-100`. Touch devices have no hover, so users
couldn't send a freshly-created note (or change its color, label,
archive it).

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - Add Tailwind v4 arbitrary variant `[@media(hover:none)]:opacity-100`
    to the per-card action row so it stays always-visible on touch
    devices while desktops keep the existing hover-reveal behaviour.
  - Apply the same variant to the unpinned pin button (which uses the
    identical hover-only treatment).

Tests
- artifacts/tx-os/tests/notes-card-touch-actions.spec.mjs (new)
  - Uses `test.use({ hasTouch: true, isMobile: true, viewport })` on
    chromium (webkit isn't installed) so the page reports
    `(hover: none)`. Sanity-checks that via matchMedia, then creates a
    note via the composer (tap-outside auto-save) and asserts both
    that the Send button is visible and that the action row's
    computed opacity is exactly 1 — Playwright's `toBeVisible` alone
    doesn't catch opacity-0.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm
  notes-card-touch-actions: 3 passed (touch test + the two
  delete-confirm specs from #423 to confirm no regression).
- Architect code review: no severe issues. Desktop hover-reveal is
  preserved because `(hover: none)` doesn't match on pointer devices.

Out of scope (per task plan): the trash button inside the
manage-labels dialog (different surface, dialog-only).
2026-05-06 10:39:59 +00:00
riyadhafraa 2027ee2545 notes: replace native confirm() with styled AlertDialog (task #423)
Original task: deleting a note (and a label from the manage-labels menu)
showed the browser's native confirm() with the raw Repl hostname,
clashing with the rest of the app. Switch both call sites to the
project's existing AlertDialog component, matching the pattern used in
FoldersRail and SendNoteDialog.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - NoteCard: add `confirmDeleteOpen` state; render AlertDialog as a
    Fragment sibling of the clickable card (not a child) so React's
    portal event bubbling cannot trigger the card's onEdit handler when
    the user interacts with the dialog. Also stopPropagation on the
    trash button itself.
  - LabelsDialog: add `pendingDeleteLabel` state and a sibling
    AlertDialog inside the Dialog root. Both dialogs use existing
    locale keys (notes.deleteConfirm, notes.deleteLabelConfirm,
    notes.delete, common.cancel) — no new strings needed.
  - Add data-testids: note-card-delete-{id}, note-delete-dialog-{id},
    note-delete-confirm-{id}, label-delete-{id},
    label-delete-dialog-{id}, label-delete-confirm-{id},
    notes-manage-labels-open.

Tests
- artifacts/tx-os/tests/notes-delete-confirm.spec.mjs (new)
  - Note delete: cancel keeps the card, confirm fires DELETE and
    removes the card. Deliberately does NOT register a `dialog` event
    handler — a native confirm would hang the test.
  - Label delete: open manage-labels, create a label via the UI, click
    trash, confirm, assert DELETE /api/note-labels/{id}.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm:
  2 passed.
- Architect code review surfaced a real bubble-through risk on the
  card's onClick={onEdit}; fixed by lifting the AlertDialog to a
  Fragment sibling.

Out of scope (not changed): native confirm()/alert() outside the Notes
page. (executive-meetings.tsx already uses an in-app `confirm({...})`
modal, not the native browser dialog.)
2026-05-06 10:30:28 +00:00
riyadhafraa 1ffb470e8f notes: add per-note checklist (to-do list) option
Task #420 — answers the user request "وين خيار اضيف list to do?".

Schema (lib/db/src/schema/notes.ts):
- notes + note_recipients gain `kind` (varchar(16) default 'text')
  and `items` (jsonb<ChecklistItem[]> nullable). Snapshot copy on
  note_recipients keeps delivered checklists immutable across sender
  edits/deletes.
- Drizzle push applied; lib/db .d.ts rebuilt.

API (artifacts/api-server/src/routes/notes.ts):
- ChecklistItem type + bounded parser (≤200 items, id ≤64, text ≤500).
- parseNoteInput normalizes items↔kind on create and on PATCH that
  carries `kind`; PATCH handler additionally coerces items-only
  patches against the note's existing kind so a text note can never
  end up with checklist items (and vice versa).
- POST/PATCH/send/loadRecipientsForNote/received/thread responses and
  the realtime `note_received` payload all carry kind+items, with the
  thread response falling back to the recipient snapshot.

Client (artifacts/tx-os/src/lib/notes-api.ts + pages/notes.tsx):
- Note/ReceivedNote/NoteThread/SentNoteRecipient extended with
  kind+items.
- New `ChecklistEditor`, `ChecklistView`, and `KindToggle` components.
- Composer and EditNoteDialog gain the to-do toggle (ListTodo icon)
  and switch between Textarea and ChecklistEditor; saves send
  kind+items, with empty-checklist auto-discard mirroring the existing
  empty-text behaviour.
- NoteCard, inbox list, sent list, and ThreadDialog body render the
  checklist (read-only on snapshots; owner cards can toggle done via
  PATCH with stopPropagation so the edit dialog doesn't open).

i18n: notes.checklist.{toggle,addItem,itemPlaceholder,emptyHint,
removeItem,progress} added to en.json + ar.json.

Tests: new artifacts/tx-os/tests/notes-checklist.spec.mjs covers
composer→persist→reload→toggle, items-only PATCH normalization on
both kinds, and checklist delivery to recipient snapshot. All 3 pass.

Architect review (evaluate_task) flagged one real issue: items-only
PATCH normalization. Fixed in the PATCH handler and locked in by the
new normalization test.

Pre-existing executive-meetings.ts tsc errors are unchanged and
unrelated to this task.
2026-05-06 10:12:52 +00:00
riyadhafraa d4e0dfc6e5 notes: persist chosen color (and labels) when composer auto-saves
When creating a new note, picking a color from the palette had no
effect — the note saved with the default white background. Editing an
existing note worked because that path lives in a Dialog with no
auto-save guard.

Root cause
The inline Composer in artifacts/tx-os/src/pages/notes.tsx attaches a
document-level mousedown listener that auto-saves whenever the click
falls outside its container ref. The color and label menus are built
on Radix Popover, which renders content in a portal outside the
composer's DOM tree. So clicking a swatch fired:
  1. document mousedown -> save() with the OLD color
  2. swatch click       -> setColor(new) (state already discarded)

Fix
The mousedown handler now ignores clicks whose target is inside any
Radix popper wrapper, Radix portal, or role="dialog" element. The
guard is narrow: legitimate outside clicks (page background, sidebar,
other notes) still trigger save() exactly as before. The same fix
covers the LabelMenu popover for free since it shares the abstraction.

Tests
- New tests/notes-composer-color.spec.mjs reproduces the original
  failure mode: open composer, type title, pick the "red" swatch from
  the popover, click outside, assert the POST /api/notes response has
  color === "red".
- Existing notes-folders + new spec run green together (3 passed in
  29.1s); tx-os tsc --noEmit clean.

Architect notes (followed inline / deferred)
- Suggested mirroring a labels-popover e2e — same code path is already
  exercised; can be added later if regressions surface.
- composedPath() hardening — current target.closest() works for all
  real Radix portals; not needed.
2026-05-06 09:59:10 +00:00