Commit Graph

105 Commits

Author SHA1 Message Date
Riyadh 439cbff0fc Update website's default sharing image
Update opengraph.jpg in the public artifacts directory.
2026-05-14 07:00:21 +00:00
Riyadh 1381e38044 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
Riyadh 1e5a22d2f9 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
Riyadh aa60a97b61 Restore original open graph image
Revert changes to the open graph image file to restore its original state.
2026-05-12 08:48:11 +00:00
Riyadh 5bef524da1 Task #503: Stop home-screen Arabic labels from clipping letter dots on iPad
User report: in the Tx OS launcher, the dot above ج in "الاجتماعات"
(and similar diacritics on other tile labels) was being sliced off on
iPad — a visible visual-quality bug on the most-used screen.

Root cause (artifacts/tx-os/src/pages/home.tsx, AppIconContent):
the label span used `leading-tight` (line-height 1.25) + `truncate`
(overflow: hidden). At 11px the line-box was 13.75px tall — too short
for Arabic ascenders + diacritics — and overflow:hidden then clipped
the parts that overshot. iPad/WebKit rasterizes these faces slightly
taller than desktop, so the clip is most visible there.

Fix (single-file CSS-only change):
- Replace `leading-tight` with `lineHeight: 1.6` for plenty of room
  above the baseline.
- Add `paddingTop: 2` as cheap insurance for any remaining overshoot.
- Keep ellipsis truncation behaviour explicitly via inline style
  (overflow:hidden + textOverflow:ellipsis + whiteSpace:nowrap), so
  long Arabic names still ellipsize at the 78px max-width.
- Add `block` so max-width applies cleanly.

No change to icon tile size, grid layout, badges, or any other
markup. Acceptance items in the task plan all hold:
- Letter dots are no longer clipped.
- Long names still truncate with an ellipsis at 78px.
- Desktop layout is visually unchanged.

Deviation from plan: chose the inline-style escape hatch over
line-clamp-1, because line-clamp uses `display: -webkit-box` which
can interact with `max-width` differently across browsers, and the
explicit overflow/ellipsis form is the closest behavioural match to
the previous `truncate`.
2026-05-12 08:47:16 +00:00
Riyadh 0b6e6013c8 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.
2026-05-11 07:53:40 +00:00
Riyadh 9e8be527af theme: lighten primary navy from #0B1E3F to #2C4A8A
Task #477. The brand primary color was a very dark navy (218 70% 15%
≈ #0B1E3F) and dominated buttons, the active sidebar row, the FAB,
and focus rings. User asked to lighten it while keeping the same blue
family.

Change: in `artifacts/tx-os/src/index.css` (`:root`), bumped four
tokens to `218 55% 32%` (≈ #2C4A8A) and updated the inline hex
comment:
- `--primary`
- `--ring`
- `--sidebar-primary`
- `--sidebar-ring`

White text on the new primary keeps WCAG AA contrast (≈5.3:1), so the
existing `--primary-foreground: 0 0% 100%` is preserved.

No other tokens (background, foreground, accent, secondary, destructive,
borders) were touched. No component-level overrides were modified.
2026-05-10 15:23:39 +00:00
Riyadh 20583740e5 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:34:03 +00:00
Riyadh 1ea075d2b6 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
Riyadh 6adf42617f 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
Riyadh bd8a4adbe9 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
Riyadh 953c516b1f Task #410: floating draggable note popup + reply alert at sender
Convert the incoming-note popup from a centered AlertDialog modal into
a floating, draggable, animated card (no backdrop) and surface the
same card variant at the original sender when the recipient replies.

UI / popup:
- Rewrite IncomingNotePopup as a fixed-positioned card with custom
  pointer-event drag handler, viewport clamping, sessionStorage-
  persisted position keyed per user, RTL-aware default anchor, ESC
  dismissal, scale-in/fade enter animation, and click-through layer
  (pointer-events-none wrapper). Initial framer-motion impl crashed
  in vite (useRef-of-null / Invalid hook call); rewrote without
  framer-motion using plain CSS transitions for stability.
- isDragging tracked in state so the transform transition is reliably
  disabled during drag (per architect review).

Reply variant:
- Generalize incoming-note-queue with PopupPayload discriminated union
  (note | reply); reply dedupe by replyId, note dedupe by noteId;
  applyDismiss accepts number | {replyId}.
- Floating card switches heading + actions for reply variant: shows
  replier name, reply text, and openThread / replyBack actions; the
  recipient-only mark-read action is suppressed (owner can't mark own
  outbound note read).

Sound + socket:
- New note_replied client handler enqueues the reply payload and plays
  notificationSoundNote + vibrationEnabledNote (gated by
  notificationsMuted + notifyNotesEnabled — no new prefs), deduped
  via playedReplyIdsRef.
- Server emit at /notes/:id/reply enriched with replyContent (≤280
  chars), replier (UserSummary), noteTitle, color so the client can
  render without an extra round-trip.

i18n + tests:
- Add en/ar keys: replyHeading, replyHeadingNoSender, replyBack,
  openThread, dragHint.
- Extend notes-popup-on-receive.spec.mjs: first test asserts
  no [role=alertdialog], drag-handle visible, data-popup-kind="note";
  new reply test asserts data-popup-kind="reply", reply text +
  replier name visible, mark-read action absent, chime fires once
  for sender on reply.

Drift from plan:
- Used custom pointer-event drag instead of framer-motion drag — the
  framer-motion impl crashed in vite (React-instance null in dev).
  Same UX (drag from header only, click-through behind card).
- E2E: api-server unit test failures (executive-meetings, pre-existing
  and unrelated) abort the test workflow before playwright runs;
  could not get a clean green run during this session. Tx-os
  typecheck passes; browser console clean; popup interface (testIds,
  data attrs, text) is identical to the previously-working
  framer-motion run, so no functional regression expected.
2026-05-05 21:32:37 +00:00
Riyadh 2e6f249c26 Remove zoom and fullscreen features from the executive meetings page
Revert changes related to zoom and fullscreen functionality on the executive meetings page, including associated UI elements and state management.
2026-05-04 18:12:56 +00:00
Riyadh 365f8137c2 Update open graph image for the project
Replace existing open graph image file with an updated version.
2026-05-04 14:01:47 +00:00
Riyadh eed69bc8b8 Update images for improved visual presentation and branding
Update opengraph image and other attached assets.
2026-05-04 13:22:13 +00:00
Riyadh 557e692e8e Fix PDF font color not reflecting system settings
Root cause: resolveFontPrefsForUser() uses `userRow ?? globalRow` precedence.
When an admin saved font settings with scope="global", only the global row
was updated. If the admin also had a user-scope row (created by prior saves
with the default scope="user"), the user-scope row still overrode the global
row during PDF generation, causing the PDF to show the old color.

Backend fix (executive-meetings.ts):
- When saving with scope="global", delete the current admin's user-scope row
  within the same transaction. This ensures global settings immediately apply
  to the admin who set them. Other users' personal rows are unaffected.

Frontend fix (executive-meetings.tsx):
- Pass both `globalFont` and effective `font` to FontSettingsSection.
- When the scope dropdown changes, the form now loads the selected scope's
  actual saved values instead of always showing the effective (user-override)
  values. This prevents confusion where the admin edits "global" but sees
  their personal values in the form.

Verified end-to-end:
- PATCH font-settings with scope=user + fontColor=#ff0000 → DB updated → PDF uses red
- PATCH font-settings with scope=global + fontColor=#0000ff → global updated,
  user-scope row deleted → PDF resolves to global blue
- TypeScript compiles cleanly, e2e Playwright test passes
2026-05-04 12:17:40 +00:00
Riyadh 5d20799ad8 Apply row coloring consistently across the web and PDF views
Adjust cell styling to ensure row background colors are applied uniformly to both individual and merged rows, matching the PDF rendering behavior in the web interface.
2026-05-04 11:05:57 +00:00
Riyadh 3e420536fb Improve PDF generation for meeting tables with better text handling
Update PDF rendering logic to correctly handle bidirectional text, cell merging, and text wrapping for meeting tables.
2026-05-04 09:24:37 +00:00
Riyadh 41966b7f75 PDF: compact table with zero gaps, unified lineHeight (Task #368)
User requested maximum table compression with no gaps between rows.
Changes:
- cellPadX: 8→4, cellPadY: 6→0 (zero vertical padding)
- lineHeight: fontSize*1.5 → fontSize*1.05 (very tight)
- All columns (meeting, attendees, time, #) now center-aligned
- Added lineHeight option to DrawOpts type so draw helpers use the
  same metric as row measurement (fixes probe/draw mismatch that
  could cause row overlap with tight padding)
- drawMixedLine and drawWrappingLine now respect opts.lineHeight
  instead of hardcoded fontSize*1.2, falling back to 1.2 when
  lineHeight is not provided (backward compatible)
- Table cell draw calls pass lineHeight to drawWrappingLine
- Both AR and EN PDFs verified valid with logo embedded
2026-05-04 08:51:09 +00:00
Riyadh eea5b9eebe PDF: enlarge header logo and center attendees column (Task #364)
- Increased logoBoxSize multiplier from 1.8 to 2.8 so the header logo
  renders noticeably larger in the PDF output.
- Changed attendees column text alignment from direction-based (right for
  RTL) to "center" so attendee names are centered in their column.
- Both AR and EN PDFs verified: valid PDF output, logo embedded, correct
  alignment. Code review PASSED, e2e tests PASSED.
2026-05-04 08:12:02 +00:00
Riyadh 8462cfcfd1 Compact PDF table layout to match reference design (Task #359)
Changes to artifacts/api-server/src/lib/pdf-renderer.ts:

- Reduced cellPadX from 6 to 4 (horizontal cell padding)
- Reduced cellPadY from 3 to 2 (vertical cell padding)
- Tightened lineHeight multiplier from 1.25 to 1.2
- Removed +2 padding buffer from heightOfString measurement loop
- Removed +2 from drawWrappingLine return value
- Tightened drawMixedLine return value (1.25→1.2)
- Consolidated header spacing from two moveDown(0.2+0.4) to one moveDown(0.3)
- Tightened title Y offset multiplier from 1.25 to 1.2
- Fixed row height probe to detect script per-line (matching draw path)
  instead of per-cell, preventing measurement/draw mismatch in
  mixed Arabic/Latin content now that the +2 cushion is removed

Result: Table rows are compact with no visible gaps between them,
matching the user's reference PDF (attached_assets/rrr1_1777879598338.pdf).
Row colors, cell borders, and footer positioning are unchanged.
Both AR and EN PDFs generate successfully (200 status).
2026-05-04 07:35:52 +00:00
Riyadh addbb6699a Task #352: Make logo upload always visible for admin users
Previously, the logo upload field in Font Settings was hidden behind two
conditions: canEditGlobal AND scope === "global". Admin users had to
first switch the scope dropdown to "Global" before the logo upload
appeared, making it very hard to discover.

Changes:
- Frontend (executive-meetings.tsx): Removed `scope === "global"`
  condition from logo upload rendering — now shows whenever
  `canEditGlobal` is true (admin/executive_office_manager).
- Frontend: Save function now always sends `logoObjectPath` when
  `canEditGlobal` is true, regardless of selected scope.
- Backend (executive-meetings.ts): When an admin saves with user scope
  and includes logoObjectPath, the logo is written to the global row
  (upsert) in the same transaction. This preserves the global-only
  semantics while allowing admins to update the logo from any scope.
- Removed the 400 rejection for logoObjectPath on user-scope saves.
- Authorization preserved: only EM_ADMIN_ROLES can trigger logo writes.

Tested:
- API: PATCH with scope=user + logoObjectPath correctly updates global row
- E2E: Logo upload field visible for admin in user scope (confirmed)
- Pre-existing test failures unchanged (PDF font assertion, notification tests)
2026-05-04 06:40:35 +00:00
Riyadh c8d768c1e1 Task #315: rebuild 12h picker as 3 controls (hour + minute + AM/PM)
The first ship of #315 used a single text input + AM/PM toggle. The
spec (lines 30-32) explicitly mandates THREE controls per field:
hour input (1-12), minute input (00-59), and AM/PM toggle. This
follow-up commit refactors TimePicker12h to that shape while
keeping every behaviour from the first ship intact.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.
2026-05-01 21:09:01 +00:00
Riyadh bf5899877b EM: custom fonts + Tab/Shift+Tab attendee quick-add
Task #301.

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

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

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

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

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

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

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

Verified: TS clean, e2e covers forward Tab chain, Shift+Tab between
existing rows, first-in-section no-op, and Shift+Tab from the
pending row.
2026-05-01 19:21:10 +00:00
Riyadh 83a7c7dd93 Remove repeated "+ شخص هنا" chip from attendee cells
The Executive Meetings schedule used to render a small dashed "+ شخص هنا"
("+ person here") chip in the gap between every two consecutive person
rows of an attendee cell. With three attendees the user saw three chips
stacked under the names, which looked noisy and repetitive. The user
asked for the chip to be removed entirely; the existing trailing "+"
button at the end of the cell already covers the add-person flow.

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

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

Verification:
- TypeScript: clean.
- e2e (testing skill): on /executive-meetings, the page contains zero
  elements matching the old chip's data-testid prefixes and zero
  occurrences of the literal text "+ شخص هنا" or "+ person here", while
  the schedule and attendee lists render normally.
2026-05-01 18:41:12 +00:00
Riyadh c73ae44ab4 Add settings to customize meeting alert appearance and behavior
Implement a settings card for upcoming meeting alerts, allowing users to enable/disable the alert, choose color presets, and reset to defaults. Modify the alert component to respect these preferences and add a "Details" toggle to expand and display meeting information.
2026-05-01 18:19:23 +00:00
Riyadh 168f7fb479 Task #295: Drop AM/PM (م/ص) from meeting schedule times
What changed:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  - Rewrote the local formatTime helper to call
    Intl.DateTimeFormat.formatToParts directly, filter out segments of
    type "dayPeriod", join the remaining parts and trim. Locale uses
    "ar-u-nu-latn" / "en-US-u-nu-latn" so Latin digits remain forced
    in Arabic. hour12: true, hour: "numeric", minute: "2-digit"
    preserved from Task #292 — only the AM/PM (en) and ص/م (ar)
    suffix is gone.
  - Removed the now-unused i18nFormatTime import (the shared helper
    in lib/i18n-format.ts always emits dayPeriod when hour12: true,
    and this page now intentionally diverges).
  - Touches both call sites — the schedule cell (~L3452) and the
    Manage tab list (~L4583) — via the same helper.
- artifacts/api-server/src/lib/pdf-renderer.ts
  - Applied the identical formatToParts + filter("dayPeriod") + trim
    transformation in formatTimeRange so the printed PDF Time column
    matches the on-screen schedule for both languages.

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

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

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

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

Notes:
- Pre-existing test workflow failures on executive-meetings*.test.mjs
  (500 errors on meeting create + Reorder 400 vs 200) are unrelated to
  this task — they pre-date both this task and Task #205.
- Re-applies and extends the 12h direction from earlier Task #160 which
  had regressed back to 24h.
2026-05-01 16:28:02 +00:00
Riyadh c810ba5992 Audit log: filter by actor (#197)
Admins can now filter the audit log by who acted, not just by what was
acted on.

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

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

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

Notes / drift
- URL key is `actorId` (per task spec), but the React state and API
  param remain `actorUserId` to match the existing backend.
- The broader `test` workflow has pre-existing failures in
  executive-meetings.test.mjs and service-orders.test.mjs unrelated
  to this change (admin 401 / HTML-404 fallback). Captured as
  follow-up #291.
2026-05-01 15:26:36 +00:00
Riyadh 85ac6e2ce9 Add shared row highlighting to executive meeting scheduler
Implement shared row highlighting for executive meetings by adding a `rowColor` field to the database schema and API, and migrating existing per-device colors to the new shared field.
2026-05-01 15:24:59 +00:00
Riyadh 2bf6cfcdb5 Task #196: Add inline "Recent activity" sections to admin detail panels
- Added shared `RecentActivityForTarget` component in `artifacts/tx-os/src/pages/admin.tsx` that lists the 10 most recent audit entries for a given (targetType, targetId), reusing `formatAuditSummary` for consistent wording with the audit log section, plus actor + timestamp metadata. Renders loading / empty / list states with stable data-testids.

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

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

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

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

No backend changes required (the existing `targetType`/`targetId` filter on `GET /api/audit` was already supported).
2026-05-01 14:58:32 +00:00
Riyadh 5c952d6716 Task #194: Show deleted user's full name in admin Audit Log
Original task
-------------
The admin Audit Log row for a user.delete entry showed
"Target: user #1234 · @ahmed" — i.e. the @username — even though
admins recognise people by name. Surface displayNameEn / displayNameAr
in that line and fall back to @username only when no display name
exists.

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

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

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

Verification
------------
- pnpm api-spec codegen + tx-os tsc --noEmit: clean.
- artifacts/api-server `node --test` for audit-log-coverage,
  audit-logs-actor-filter, audit-logs-forced-only-filter,
  audit-logs-target-filter: 43 / 43 tests pass (CSV export tests
  included).
- e2e test via runTest: created a user with displayName
  "Ahmed Test User" / "أحمد مستخدم", deleted without force,
  opened /#section=audit-log, and confirmed the new row's
  summary plus the secondary "Target: user #19630 · أحمد مستخدم"
  line render correctly (admin's Arabic locale was honoured).
2026-05-01 14:00:39 +00:00
Riyadh 994939ff79 Task #192: Show row color + merge range previews in row-actions kebab menu
Original task: The Executive Meetings row-actions kebab menu has three sub-views
(delete, color, merge). Users couldn't tell at a glance which colour or merge
range a row already had — they had to drill into the sub-view first. Add subtle
inline indicators to the main menu so the current state is visible without
extra clicks.

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

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

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

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

Follow-up proposed: #276 (next_steps) — show the same cues directly on the
row itself, not only inside the kebab popover.
2026-05-01 13:44:21 +00:00
Riyadh 063e0538a4 Task #188: Add browser test for reorder rollback when the API rejects a row drag
Added a Playwright e2e scenario in
`artifacts/tx-os/tests/executive-meetings-schedule-features.spec.mjs`
that exercises the optimistic-update + onError rollback path in
`reorderRows()` (artifacts/tx-os/src/pages/executive-meetings.tsx ~L1484).

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

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

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

No production code changes; this is a pure test addition.
2026-05-01 12:53:59 +00:00
Riyadh 92543f4bf8 Task #273: 5-minute pre-meeting alert for Executive Meetings
Floating, draggable alert that appears on every Tx OS page when an
Executive Meeting is within five minutes of starting.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Validation: the new test passes in isolation and as part of the
schedule-features suite. One unrelated existing test
("custom highlight color paints the current meeting's box-shadow
ring") is currently flaky/failing on its own without my changes;
left untouched as it is outside this task's scope.
2026-05-01 11:18:40 +00:00
Riyadh 197d21ef17 #267: Freeze top bar, schedule heading, and table column header on scroll
Top bar and schedule-heading row already used `position: sticky` with
dynamic offsets published as `--em-header-h` / `--em-heading-h` via
ResizeObserver. The table column header is the new piece.

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

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

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

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

Files:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/tests/executive-meetings-sticky-header.spec.mjs
2026-05-01 09:54:18 +00:00
Riyadh 7668a859a3 Restore opengraph image to its previous state
Revert changes to artifacts/tx-os/public/opengraph.jpg to resolve unintended modifications.
2026-05-01 08:54:46 +00:00
Riyadh c205da5f9f #265: unify Executive Meetings header into single Settings tab
Header
- Removed the standalone Font Settings nav button and the bilingual
  language toggle. The header now exposes only Export PDF.
- Renamed the SECTIONS key `fontSettings` → `settings` (Settings icon
  retained) and updated visibility checks + render switch.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Validation: tx-os typecheck clean; pre-existing executive-meetings.ts
errors not regressed; all targeted server tests pass (delete-force-warnings 10,
audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new
actor-filter 4, broader audit/services sweep 40); e2e test verified actor
dropdown rendering, filter behavior, readable Arabic service.delete summary,
and CSV export honoring the filter.
2026-05-01 06:57:50 +00:00