Commit Graph

22 Commits

Author SHA1 Message Date
riyadhafraa 471c480824 Task #303: DIN Next LT Arabic site default + working font picker
User report: "the dropdown for changing the site font does not work."
Root cause: FontSettingsSection listed Cairo / Tajawal / Noto Naskh
Arabic / Amiri, but only Tajawal had been registered with @font-face.
Picking the others was a no-op. Site default body font was also not
DIN Next LT Arabic as designed.

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

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

Verification:
- Frontend tsc clean.
- Backend tsc shows the same pre-existing unrelated errors as before
  (isHighlighted boolean/number; Drizzle scope typing).
- E2E browser test verified all 7 assertions: site default font,
  no Google Fonts requests, exactly 6 dropdown options, font picker
  applies + reverts correctly, attendee font picks persist after save.
- Code review verdict: PASS.
2026-05-01 19:55:39 +00:00
riyadhafraa efe74f150a #262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections, including the
canApprove capability flag.

Backend
- routes/executive-meetings.ts: deleted /requests* + /tasks* handlers,
  REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES, the four
  retired capability flags from /me, retired imports, and dead schemas
  (detailsByType, request*Schema, taskCreateSchema, taskPatchSchema,
  dueAtSchema, dateOnly, timeHm). Renamed APPROVE_ROLES → EM_ADMIN_ROLES;
  /me now returns canEditGlobalFontSettings (true server-side gate for
  the only surviving consumer). Dropped now-unused requireApprove export.
- lib/executive-meeting-notify.ts: types collapsed to ['meeting_created'].

Frontend
- pages/executive-meetings.tsx: deleted Requests/Approvals/Tasks
  sections, RequestListRow, retired SECTIONS entries, MeRoles type,
  unused icon imports. MeCapabilities.canApprove → canEditGlobalFontSettings.
- hooks/use-notifications-socket.ts: dropped two retired invalidations.
- locales/{ar,en}.json: removed nav + section + 6 retired type keys.

Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables/relations removed.
- scripts/cleanup-em-requests-tasks.sql: idempotent cleanup — orphan
  prefs / notifications / audit rows then DROP TABLE … CASCADE.
  Applied to dev DB; `db push` re-synced.

Tests
- Sequential `node --test --test-concurrency=1` → 226/226 pass.
- /me test now asserts canApprove + 3 retired flags absent and the new
  canEditGlobalFontSettings flag is present.
2026-05-01 08:36:43 +00:00
riyadhafraa 389e8b785c #262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections.

Backend
- routes/executive-meetings.ts: deleted /requests* + /tasks* handlers,
  REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES, the three
  capability flags from /me, retired imports, and dead schemas
  (detailsByType, request*Schema, taskCreateSchema, taskPatchSchema,
  dueAtSchema, dateOnly, timeHm). canApprove kept (FontSettings).
- lib/executive-meeting-notify.ts: types collapsed to ['meeting_created'].

Frontend
- pages/executive-meetings.tsx: deleted Requests/Approvals/Tasks
  sections, RequestListRow, retired SECTIONS entries, MeRoles type, and
  unused icon imports.
- hooks/use-notifications-socket.ts: dropped two retired invalidations.
- locales/{ar,en}.json: removed nav + section + 6 retired type keys.

Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables/relations removed.
- scripts/cleanup-em-requests-tasks.sql: idempotent cleanup — orphan
  prefs / notifications / audit rows then DROP TABLE … CASCADE.
  Applied to dev DB; `db push` re-synced.

Tests
- Sequential `node --test --test-concurrency=1` → 226/226 pass.
- 3 pre-existing parallel-file pollution failures in the workflow
  runner are unrelated to #262 (verified by sequential run).
- Pre-existing tsc warnings at routes L509/625/L1594 untouched.
2026-05-01 08:28:11 +00:00
riyadhafraa 21c935064d #262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections — UI, locales, realtime
invalidations, backend routes, role lists, capability flags, schema
tables, notify lib, and tests.

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

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

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

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

Drift / pre-existing
- 3 test failures observed under the parallel `node --test` workflow
  (meeting_created fan-out count, pref opt-out daily-number conflict,
  service-orders JSON-vs-HTML) are pre-existing parallel-file
  pollution between executive-meetings.test.mjs and
  executive-meetings-notifications.test.mjs. Verified by running
  `node --test --test-concurrency=1 'tests/**/*.test.mjs'` →
  226/226 pass. Out of scope for #262.
- Pre-existing tsc warnings at routes/executive-meetings.ts L509/625
  (boolean/number on isHighlighted) and L1594 (font-settings scope
  query) untouched.
2026-05-01 08:18:29 +00:00
riyadhafraa c53641c721 #245: narrow umbrella subset — toast polish, opt-out tests, Restore defaults
Picked the 3 most isolated items from the 7-item umbrella; deferred the rest
as #259/#260/#261.

#223 + #224 — singular toast + summary on partial failure (T001):
- my-orders.tsx: replaced the N=1 vs N>1 ternary in scheduleDelete with a
  single t("myOrders.clearedCount", { count }) so i18next picks _one /
  _other automatically. Single-row delete now flows through this same toast
  too — user-visible copy for N=1 is now "1 order deleted" / "تم حذف طلب
  واحد" instead of the legacy "Order deleted" / "تم حذف الطلب".
- my-orders.tsx: partial-failure path now shows ONE summary toast using
  the existing clearedPartial key ("{{ok}} deleted, {{fail}} failed")
  instead of N error toasts. Total failure (okCount===0) keeps deleteFailed.
- Updated 3 Playwright specs that asserted the legacy copy:
  order-clear-finished-undo (already had the singular case), order-undo-toast
  (Arabic single-row delete), order-delete-flush-on-unmount (English).
  Note: the legacy "myOrders.deleted" locale key is now unreferenced in
  source — left in place to avoid noise; deletion can be handled separately.

#238 — opt-out coverage in executive-meetings-notifications.test.mjs (T002):
- Appended 4 tests + setPref/clearPref helpers covering
  filterRecipientsByNotificationPref: inApp=false drops user, missing pref
  defaults to ON, cross-event isolation (mute on event A leaves event B
  alone), email=false leaves in-app intact. Helpers use ON CONFLICT on the
  verified unique index. Some scenarios overlap existing tests in
  executive-meetings.test.mjs (lines 1764, 1809) — these still add value
  by exercising the meeting_created socket fan-out path and cross-event
  isolation, which the existing tests don't cover.

#236 — Restore defaults endpoint + button (T003):
- Server: added DELETE /api/executive-meetings/notification-prefs after PUT.
  Scoped strictly to req.session.userId, returns {ok, count}. Reuses
  requireExecutiveAccess guard. Architect confirmed no cross-user leakage.
- Client: restoreDefaults() handler + outline button (data-testid
  "em-pref-restore-defaults", NOT gated on dirty since the whole point is to
  blow away saved settings). New i18n keys restoreDefaults / restored in
  both locales.
- Architect found a stale-state race in restoreDefaults: setDraft(null)
  was called before invalidateQueries, letting the seed effect repopulate
  draft from still-cached pre-DELETE data. Fixed by inverting the order to
  match save() — invalidate first (await refetch), then setDraft(null).
- Tests: appended 2 integration tests to executive-meetings.test.mjs
  covering the full restore flow (PUT 2 muted prefs → DELETE → assert
  {ok,count:2} + GET shows defaults + actual fan-out reaches user again)
  and idempotent no-op DELETE on a user with no rows.

Test results:
- executive-meetings.test.mjs: 47/47 pass (incl. 2 new DELETE tests)
- executive-meetings-notifications.test.mjs: 11/11 pass (incl. 4 new opt-out tests)
- Playwright order specs: 6/6 pass after legacy-copy updates
- Pre-existing failures in service-orders + meeting_created fan-out are
  untouched and not caused by this change.

Follow-ups proposed: #259 (beforeunload + tab-close Playwright), #260
(admin override another user's prefs with audit row + UI), #261 (iPad
header verification — may already work).
2026-05-01 07:30:00 +00:00
riyadhafraa 3b35c29b9f Refine text sanitization and update test descriptions
Improve text sanitization logic and update comments in test files to be more concise and informative.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 316e780f-e839-41c5-9826-be64a0fe9d70
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dYJU04s
Replit-Helium-Checkpoint-Created: true
2026-04-30 23:31:57 +00:00
riyadhafraa 8b6eed1e59 EM #241: sanitize location/meetingUrl/notes (regex stripper) + tests
Original task: Executive Meetings — test coverage + sanitization
closeout (umbrella for #170, #186-189, #201, #202, #212, #214, #218,
#235).

What landed
- Added `stripTagsToPlainText[OrNull]` in
  `artifacts/api-server/src/lib/sanitize.ts`. Implementation is a
  two-pass regex stripper (NOT sanitize-html + entity decode):
    Pass 1: drop dangerous tag bodies entirely
            (`<script>`/`<style>`/`<noscript>`/`<iframe>`/`<object>`/
             `<embed>`/`<template>` content + tags).
    Pass 2: strip HTML comments, CDATA, DOCTYPE, processing
            instructions, and any remaining open/close tags via
            `<\/?[a-zA-Z][^>]*>`.
  No entity decode pass — so URLs (`?a=1&b=2`) round-trip unchanged,
  plain text (`5 < 10`) is preserved, AND attacker-supplied encoded
  payloads (`&lt;script&gt;…`) survive as inert text instead of being
  rehydrated into live tags. The existing `sanitizePlainText` (which
  entity-encodes) is preserved for `attendee.title` so the print
  template's HTML interpolation behavior is unchanged.
- Wired the new helper into all 11 write paths for
  `location`/`meetingUrl`/`notes` in
  `artifacts/api-server/src/routes/executive-meetings.ts`:
  POST /executive-meetings, PATCH /executive-meetings/:id,
  POST /executive-meetings/:id/duplicate, and `applyApprovedRequest`
  (`change_location` + `note`). attendee.title call sites kept as-is.
- Added 6 API tests in
  `artifacts/api-server/tests/executive-meetings.test.mjs`:
    1. POST sanitization (URL `&` round-trip + literal `<`/`&` in notes
       + asserts NO entity-encoding in stored values).
    2. Encoded-payload regression guard (`&lt;script&gt;`,
       `&#x3C;script&#x3E;`, mixed-case `<ScRiPt>`/`<IFRAME>`)
       confirming we don't decode entities into live tags.
    3. PATCH sanitization on the same fields.
    4. Duplicate-path round-trip (URL ampersands preserved).
    5. change_location approved-request round-trip + tag stripping.
    6. EditableCell column-independence contract test (PATCH titleEn
       alone must not clobber titleAr and vice versa).

Drift from the original umbrella
- The umbrella also listed 9 Playwright e2e specs (#170, #186, #187,
  #188, #201, #212, #214, #218, #235). Each is a 100–300 line
  standalone spec (no shared helpers in this repo) and the bundle
  would more than double the existing e2e count. Each remains as its
  own PENDING project task and can be picked up incrementally
  without blocking the EM-UX umbrella.

Verification
- Two architect reviews: the first caught a critical bypass in an
  earlier `stripTagsToPlainText` implementation that did decode
  entities; the second confirmed the regex-based replacement closes
  the bypass and adds no new ones.
- Suite: 226 tests, 224 passing. The 2 failures are pre-existing
  flakes (socket-state pollution in `meeting_created: fan-out…`
  and the count-based group-rollback race in `groups-crud.test.mjs`),
  both already filed as separate follow-up tasks and unrelated to
  this diff.
- Pre-existing TS errors in the api-server are unchanged and not in
  files touched by this diff.
2026-04-30 23:19:40 +00:00
riyadhafraa ff220770bb Improve sanitization for meeting details to prevent malicious input
Introduce a new function `stripTagsToPlainTextOrNull` to sanitize location, meeting URL, and notes fields, ensuring HTML tags are removed while preserving special characters for proper URL and text rendering. This change enhances security by preventing cross-site scripting (XSS) attacks and ensures data integrity for these fields across create, update, and duplication operations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: cee83569-351b-48ea-ade1-9ebfdd9d85eb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dYJU04s
Replit-Helium-Checkpoint-Created: true
2026-04-30 20:58:11 +00:00
riyadhafraa 1b40d7dc00 fix(executive-meetings): lock down attendee save payload (#221)
Why:
Task #220 added a client-only `_sid` field on attendee rows so React
DnD can identify rows. The two client save sites already enumerate
wire fields explicitly and never serialize `_sid`, but nothing
prevents a future refactor from accidentally leaking it. The server
was zod-default lenient (silently strips unknowns), so a regression
would either be silently absorbed (bad — silent contract drift) or
land in a future JSONB metadata column without anyone noticing.

What changed:
- `attendeeSchema` in artifacts/api-server/src/routes/executive-meetings.ts
  is now `.strict()`. Any unknown attendee key (including `_sid` or
  any future client-only field) is rejected with HTTP 400 instead of
  being silently stripped. The schema is reused by all three
  attendee-bearing endpoints (POST /executive-meetings,
  PATCH /executive-meetings/:id, PUT /executive-meetings/:id/attendees),
  so all three are covered by one change.

Tests:
- Added three API tests in
  artifacts/api-server/tests/executive-meetings.test.mjs:
  1. POST /executive-meetings with attendee carrying `_sid` returns
     400 and the error mentions the rejected key.
  2. PATCH /executive-meetings/:id with attendees carrying `_sid`
     returns 400 AND the meeting's existing attendee list is
     preserved (no partial mutation).
  3. PUT /executive-meetings/:id/attendees with attendee carrying
     `_sid` returns 400 AND the seeded attendee is unchanged.

Verification:
- Full API test suite: 207/207 green (was 204/204 before; +3 new).
- No client-side change needed: existing `saveAttendeeName` (~L943
  in artifacts/tx-os/src/pages/executive-meetings.tsx) and the
  manage-dialog save (~L4004) already project to the documented
  wire shape (`name, title, attendanceType, sortOrder, kind`).
- Architect review: addressed the one gap (PATCH coverage) by
  adding test #2 above; verdict resolved.

Out of scope cleanup:
- Marked the descriptions of stale tasks #172 and #179 as STALE
  (both PDF tests pass and PDF export works in current main).
  Final cancellation left to the user.
2026-04-30 19:16:00 +00:00
riyadhafraa fb2d75ecd7 Task #164: Per-user notification preferences for Executive Meetings
Lets each user choose whether to receive in-app and/or email notifications
for each executive-meeting event type (meeting_created, request_submitted,
request_approved/rejected/needs_edit, task_assigned, task_completed).
Defaults to "everything on" when no preference row exists, preserving the
prior fan-out behavior for users who never visit the new UI.

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

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

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

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

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

Replit-Task-Id: 284ce15d-40d7-447e-90ca-090b44d8227b
2026-04-30 18:56:05 +00:00
riyadhafraa 628da1e448 Sanitize attendee titles at the API boundary (task #130)
Original task: attendee.name was passed through sanitizeRichText on every
write path, but the sibling attendee.title was treated as plain text and
inserted verbatim. The print page and any future HTML template that
interpolates a.title would have to remember to escape it. Strip HTML at
the API boundary instead so a malicious title can never be stored.

Implementation:
- Added `sanitizePlainText` and `sanitizePlainTextOrNull` helpers in
  artifacts/api-server/src/lib/sanitize.ts. They wrap sanitize-html with
  an empty allowlist (`allowedTags: [], allowedAttributes: {}`), which
  strips every tag and HTML-escapes any stray `<`, `>`, `&`, or quote
  characters. The OrNull variant preserves null for nullable columns.
- Applied `sanitizePlainTextOrNull(a.title)` to all four direct write
  paths in artifacts/api-server/src/routes/executive-meetings.ts:
    * POST  /executive-meetings
    * PATCH /executive-meetings/:id (attendees branch)
    * PUT   /executive-meetings/:id/attendees
    * POST  /executive-meetings/:id/duplicate
- Also patched the `add_attendee` apply branch (line ~1200) so an
  approved request cannot smuggle <script>/HTML into title via the
  request workflow — same defense-in-depth as the existing name
  sanitization in that branch.

Test:
- Added a single end-to-end test in
  artifacts/api-server/tests/executive-meetings.test.mjs that pushes a
  malicious title (<script>, <b onclick=...>, <img onerror=...>,
  <a href="javascript:...>) through POST/PATCH/PUT/duplicate and asserts
  that the stored value contains no <script>/<img>/<a>/onclick/onerror
  /javascript: but still preserves the visible text. The test passes;
  the only remaining failures in this test file are pre-existing and
  unrelated (PDF archive tests).

Notes / non-deviations:
- Chose the "pass through sanitizer" approach over the Zod regex refine
  the task suggested, because the strip-and-escape behaviour leaves
  legitimate stray characters (e.g. "Director < Manager") usable
  instead of returning a 400.
- Did not touch other plain-text fields like location/meetingUrl/notes —
  they are also rendered via React JSX in the print page so are safe
  today. Captured as follow-up #189 for symmetric defense-in-depth.

Replit-Task-Id: 837863ea-23a1-4d26-8261-f0b7ef6e5b0f
2026-04-30 05:56:23 +00:00
riyadhafraa e1e7f93545 Add automated tests for the Phase-2 Executive Meetings endpoints
Task #112 — locks in RBAC, transactional safety, and the router.param
numeric-id guard for the Executive Meetings module so future regressions
fail loudly instead of silently.

What was added (all in artifacts/api-server/tests/executive-meetings.test.mjs):

1. "Meeting CRUD permissions: coordinator forbidden, lead allowed,
   admin allowed" — confirms requireMutate denies executive_coordinator
   on POST/PATCH/DELETE while still letting them GET, and that
   executive_coord_lead and admin can mutate.
2. "Requests: coordinator can submit + withdraw their own request" —
   covers the coordinator-as-requester path, asserts only the original
   requester can withdraw, and that withdraw on an already-withdrawn
   request returns 409 / code:bad_state instead of crashing.
3. "Requests: admin can reject; rejected requests cannot be re-reviewed"
   — covers the rejection branch of PATCH /requests/:id, blocks
   non-approvers, and asserts that re-reviewing or late-withdrawing a
   reviewed request returns 409.
4. "Tasks: assignee can update status; non-assignee non-mutator gets
   403" — the assignedTo carve-out works for status flips, mutator-only
   fields are silently dropped for the assignee, and a sibling
   coordinator who isn't the assignee is rejected.
5. "Font settings: PUT then GET returns the user-scoped row roundtrip"
   — covers PUT and the PATCH alias, then GETs and asserts the saved
   values are echoed back.
6. "router.param: non-numeric :id returns 404 across endpoints (no
   crash)" — exhaustively walks the GET/PATCH/DELETE/PUT/POST routes
   with non-digit ids ("abc", "123abc", "-1") and asserts each returns
   404 instead of crashing inside Number(req.params.id).
7. "Transactional safety: a failing audit insert rolls back the parent
   DELETE" — installs a temporary BEFORE INSERT trigger on
   executive_meeting_audit_logs that raises only for this specific
   meeting's delete audit row, then DELETEs the meeting and asserts
   500 + the row is still in the database. Trigger is dropped in a
   finally so other tests are unaffected.

Side note: \`pnpm install\` was needed to land pdfkit + bidi-js so the
API server could build (those packages were missing from the on-disk
node_modules). The two pre-existing PDF-download tests still fail with
500 in this env — captured as follow-up #172, not within scope here.

Replit-Task-Id: c0ece8b6-6584-4c4c-9655-a158be6db9f0
2026-04-29 18:27:15 +00:00
riyadhafraa 11aaaf2abe Replace executive-meetings print-to-PDF with server-side PDF generator
The "Download PDF" button on the executive-meetings page now hits a real
backend endpoint that returns a true PDF (no more browser print dialog),
respects each user's font preferences (family, size, weight, alignment)
with proper Arabic RTL shaping, and archives every download.

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

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

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

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

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

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

Replit-Task-Id: 68914058-ebd6-4670-a785-c0084fe1fc94
2026-04-29 18:01:19 +00:00
riyadhafraa 25a3ef21d2 Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 08:29:41 +00:00
riyadhafraa ee1e2a9f42 Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 08:23:16 +00:00
riyadhafraa ca1267948b Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 08:12:51 +00:00
riyadhafraa 4539933aee Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 08:10:13 +00:00
riyadhafraa 93c77f587c Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 08:03:25 +00:00
riyadhafraa 9ec0fc1f90 Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 07:53:51 +00:00
riyadhafraa 5114b207da Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, and font controls (Tiptap-backed EditableCell).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint swaps daily_number in a single transaction using a two-phase
   negative→final assignment that respects the (date, daily_number) UNIQUE
   constraint. Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage.

Backend changes
- Widened titleAr / titleEn / attendees.name to text (applied via direct
  ALTER TABLE because db:push --force is currently blocked by Task #126).
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, and reorder paths so rich text round-trips safely.
- Code-review fix: duplicate handler now re-sanitizes titleAr/titleEn.
- Code-review fix: print page no longer interpolates the unsanitized
  attendee.title into dangerouslySetInnerHTML.
- 4 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, and permission denial. Pre-existing app_permissions failures
  are unrelated and tracked by Task #126.

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar.
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell now passes the original attendee index to PUT writes so
  edits reach the right row even after grouping into virtual/internal/
  external sections.
- Print page renders sanitized HTML via dangerouslySetInnerHTML on names
  and titles (titles still rendered as plain text on the print page).
- Translation keys added in ar.json and en.json.

Drift
- Sanitization for attendee.title was identified by the architect review
  as a defense-in-depth gap and proposed as Task #130 rather than fixing
  inline; the print-page XSS path that depended on it was fixed directly.
2026-04-29 07:39:24 +00:00
riyadhafraa 371c44baba Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
  attendees column widest, tighter padding, navy/white/gray + red highlight
  only. Header label "م" → "#" (ar.json).

Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
  requests work without an existing meeting. db push applied.

Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
  requireMutate / requireApprove / requireRequest / requireAdminAudit on the
  appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
  {status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
  {action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
  single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
  meetings table to scope notifications to the selected day; returns []
  immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.

Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
  executive_*).

Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
  users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
  in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
  (needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
  (no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
  selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
  then print in one click; em-print-only and em-archive remain as separate
  buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
  (compact "field: old → new" diff with 6-row truncation and create/remove
  fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
  cells (avoids strict TFunction overload errors); apiJson rewritten to
  extract body/headers separately and stringify rawBody, eliminating the
  "Record<string, unknown> not assignable to BodyInit" TS errors. tsc
  --noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
  bilingual T table — it loads in a standalone print window before the i18n
  provider mounts, so an inline dictionary is the right pattern.

i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
  .audit.col.changes, .audit.{created, removed, moreChanges},
  .audit.entity.pdf_archive,
  .audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
  replace_attendees, done},
  .pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
  archivedAndPrinted}.

Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
  creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
  expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
  POST /:id/requests 201, PATCH /requests/:id status approval 200,
  audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
  200 with persisted data, /notifications?date=YYYY-MM-DD filters
  correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
  clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
  the date wiring on NotificationsSection had to be added afterwards
  (now done — NotificationsSection({date}) and queryKey/url include
  date).

Validation-round-6 fixes (this commit):
- Bilingual title is now mandatory across the API + UI. Backend Zod
  schema (meetingBaseFields.titleEn) requires .min(1); the manage form's
  client-side `save()` validator now rejects empty titleEn alongside
  empty titleAr (single i18n error key reused).
- Tasks: full reassign + edit flow. Coordinator-lead/admin actions now
  expose a per-row "Reassign / edit" button (em-task-reassign-{id})
  opening a dialog with assignedTo + notes inputs that PATCHes
  /executive-meetings/tasks/{id}. New i18n keys
  executiveMeetings.tasks.{reassign,reassigned} (AR + EN).
- Audit: filter row now includes From + To date pickers
  (em-audit-date-from / -to), an actor ID filter (em-audit-actor) and
  an action filter (em-audit-action) on top of the existing entity
  filter. Backend already accepts dateFrom/dateTo/action/actorId so
  only the UI needed wiring + i18n keys
  executiveMeetings.audit.filter.{from,to,actorId} (AR + EN).
- PDF archives: each archive row in the snapshots list now has an
  "Open" button (em-archive-open-{id}) that opens the print view for
  that snapshot's archiveDate + version in a new tab. New i18n key
  executiveMeetings.pdf.openArchive (AR + EN).
- Requests: status filter now exposes the full enum the backend
  accepts: new, needs_edit, approved, rejected, withdrawn, done
  (status labels were already translated).
- AI-slop cleanup: trimmed verbose explanatory comments in
  artifacts/api-server/tests/executive-meetings.test.mjs and the
  artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs.
- Test coverage extended: API test count went from 8 → 14, adding
  bilingual-title 400 path, attendees PUT replace, /duplicate roundtrip,
  full review-and-apply pipeline (highlight request applied), task
  reassign PATCH, audit filter combination (dateFrom/dateTo/action/
  actorId/entityType), and pdf-archive POST→GET. All 14 pass against
  the live API. The Manage create E2E (1 spec) still passes.

Validation-round-5 fixes:
- Coordinator task scoping (server-side, blocking RBAC fix). Added
  TASK_BROAD_VIEW_ROLES (admin + office_manager + coord_lead). Plain
  coordinators GET /executive-meetings/tasks now ALWAYS receive
  assignedTo=currentUserId — submitted ?mine=0 / ?assigneeId=other are
  ignored. /me also exposes a new canViewAllTasks flag for the UI.
- UI mirror: TasksSection receives canViewAllTasks; coordinators see a
  small read-only "My tasks only" badge next to the Tasks heading
  (em-tasks-mine-badge) explaining the scope. New i18n keys
  executiveMeetings.tasks.{myTasksOnly,myTasksOnlyHelp} (AR + EN).
- Seed: scripts/src/seed.ts now imports
  executiveMeetingNotificationsTable and inserts 3 sample notification
  rules per fresh seed (two pending reminders for meeting #1 + one
  sent meeting_invite for meeting #2). Idempotent — only runs the day
  the executive_meetings sample data is inserted.
- Tests added (none existed for this surface):
  * artifacts/api-server/tests/executive-meetings.test.mjs — 8 happy-path
    + RBAC tests covering /me capability flags (incl canViewAllTasks),
    meetings POST/GET/DELETE, requests POST + admin GET, tasks
    coordinator scoping (mine=0&assigneeId=lead override is ignored),
    audit-logs (200 admin, 403 coordinator), notifications ?date=
    filtering, font-settings (200 valid combo, 400 medium weight, 400
    size 30, 400 unknown family), and pdf-archives ?date=. All 8 pass.
  * artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs —
    Playwright UI test that logs in as admin, opens the Manage tab via
    em-nav-manage, fills the create dialog (titleAr + titleEn + date),
    saves, asserts POST /api/executive-meetings returns 201, and
    verifies the row landed in the DB with the expected title and date.

Validation-round-4 fixes:
- Manage attendee editor: added per-row title input alongside name +
  deterministic ChevronUp/ChevronDown reorder controls (sort order is
  recomputed on save). New i18n keys
  executiveMeetings.manage.attendees.{moveUp,moveDown}.
- Manage tab: per-row Copy button opens a new "Duplicate to date" dialog
  that calls POST /executive-meetings/:id/duplicate with a target date
  picker; on success it switches the day view to the chosen date. New
  i18n keys executiveMeetings.manage.{duplicate, duplicateToDate,
  duplicated, duplicateFailed}.
- Print page (executive-meetings-print.tsx) now uses react-i18next with
  new locale block executiveMeetings.print.* (AR + EN parity), forces
  i18n.changeLanguage(lang) so ?lang=en always prints English. Attendees
  cell in the print table is now textAlign: center (matches the Step 1
  schedule).
- Font settings tightened to spec: families = system/Cairo/Tajawal/
  Noto Naskh Arabic/Amiri (dropped Inter, monospace), weights =
  regular/bold (dropped medium, semibold), alignment = start/center
  (dropped end), size range = 12–22 (was 10–32). Enforced server-side
  in fontSettingsSchema (Zod) and mirrored in the UI selects/range.
  Verified: PATCH font-settings returns 200 for valid combo, 400 for
  fontWeight="medium", 400 for fontSize=30.

Validation-round-3 fixes:
- Tasks RBAC tightened end-to-end: new TASK_VIEW_ROLES = admin +
  office_manager + coord_lead + coordinator. GET /tasks and
  PATCH /tasks/:id now require requireTaskView. /me exposes new
  canViewTasks flag. Frontend isSectionVisible("tasks") gated on
  canViewTasks (so CEO/viewer never sees the Tasks tab or hits the
  endpoint). Verified: admin /me returns canViewTasks=true.
- GET /requests gained dateFrom + dateTo (createdAt range) and a
  requester filter that is honored only for approver/audit roles
  (non-approvers stay locked to their own requests). Verified curl
  with ?dateFrom=...&dateTo=... returns 200.
- Task dueAt schema accepts BOTH YYYY-MM-DD (from <input type="date">)
  and full ISO datetime; date-only is normalized to start-of-day UTC,
  empty string clears the field. Verified curl POST /tasks with
  {"dueAt":"2026-12-31"} returns 201 with stored
  dueAt = 2026-12-31T00:00:00.000Z.

Validation-round-2 fixes:
- GET /executive-meetings/requests: added server-side least-privilege so
  only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every
  other executive role is force-scoped to requestedBy = self regardless
  of the client's `mine` flag (closes the RBAC overexposure flag).
- isSectionVisible("tasks") now also returns true for canSubmitRequest,
  so executive_coordinator sees the Tasks tab (coordinators execute the
  follow-ups generated from approved requests).
- Requests "new request" dropdown now exposes ALL twelve backend request
  types: create, edit, delete, reschedule, add_attendee, remove_attendee,
  change_location, cancel_meeting, note, highlight, unhighlight, other.
- i18n parity: added en.json + ar.json keys for the five missing request
  types (add_attendee, remove_attendee, change_location, cancel_meeting,
  note) and the two missing statuses (needs_edit, done) under
  executiveMeetings.requests.{type,status}.
- Fixed wrong key reference: reschedule form's date row was using
  manage.field.date (does not exist) — switched to manage.field.meetingDate.

Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
  reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync,
  expanded seed data including executive_meeting_notifications sample
  rows.
2026-04-28 08:14:01 +00:00
riyadhafraa 82ae63f88e Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
  attendees column widest, tighter padding, navy/white/gray + red highlight
  only. Header label "م" → "#" (ar.json).

Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
  requests work without an existing meeting. db push applied.

Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
  requireMutate / requireApprove / requireRequest / requireAdminAudit on the
  appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
  {status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
  {action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
  single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
  meetings table to scope notifications to the selected day; returns []
  immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.

Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
  executive_*).

Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
  users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
  in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
  (needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
  (no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
  selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
  then print in one click; em-print-only and em-archive remain as separate
  buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
  (compact "field: old → new" diff with 6-row truncation and create/remove
  fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
  cells (avoids strict TFunction overload errors); apiJson rewritten to
  extract body/headers separately and stringify rawBody, eliminating the
  "Record<string, unknown> not assignable to BodyInit" TS errors. tsc
  --noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
  bilingual T table — it loads in a standalone print window before the i18n
  provider mounts, so an inline dictionary is the right pattern.

i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
  .audit.col.changes, .audit.{created, removed, moreChanges},
  .audit.entity.pdf_archive,
  .audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
  replace_attendees, done},
  .pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
  archivedAndPrinted}.

Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
  creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
  expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
  POST /:id/requests 201, PATCH /requests/:id status approval 200,
  audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
  200 with persisted data, /notifications?date=YYYY-MM-DD filters
  correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
  clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
  the date wiring on NotificationsSection had to be added afterwards
  (now done — NotificationsSection({date}) and queryKey/url include
  date).

Validation-round-5 fixes (this commit):
- Coordinator task scoping (server-side, blocking RBAC fix). Added
  TASK_BROAD_VIEW_ROLES (admin + office_manager + coord_lead). Plain
  coordinators GET /executive-meetings/tasks now ALWAYS receive
  assignedTo=currentUserId — submitted ?mine=0 / ?assigneeId=other are
  ignored. /me also exposes a new canViewAllTasks flag for the UI.
- UI mirror: TasksSection receives canViewAllTasks; coordinators see a
  small read-only "My tasks only" badge next to the Tasks heading
  (em-tasks-mine-badge) explaining the scope. New i18n keys
  executiveMeetings.tasks.{myTasksOnly,myTasksOnlyHelp} (AR + EN).
- Seed: scripts/src/seed.ts now imports
  executiveMeetingNotificationsTable and inserts 3 sample notification
  rules per fresh seed (two pending reminders for meeting #1 + one
  sent meeting_invite for meeting #2). Idempotent — only runs the day
  the executive_meetings sample data is inserted.
- Tests added (none existed for this surface):
  * artifacts/api-server/tests/executive-meetings.test.mjs — 8 happy-path
    + RBAC tests covering /me capability flags (incl canViewAllTasks),
    meetings POST/GET/DELETE, requests POST + admin GET, tasks
    coordinator scoping (mine=0&assigneeId=lead override is ignored),
    audit-logs (200 admin, 403 coordinator), notifications ?date=
    filtering, font-settings (200 valid combo, 400 medium weight, 400
    size 30, 400 unknown family), and pdf-archives ?date=. All 8 pass.
  * artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs —
    Playwright UI test that logs in as admin, opens the Manage tab via
    em-nav-manage, fills the create dialog (titleAr + titleEn + date),
    saves, asserts POST /api/executive-meetings returns 201, and
    verifies the row landed in the DB with the expected title and date.

Validation-round-4 fixes:
- Manage attendee editor: added per-row title input alongside name +
  deterministic ChevronUp/ChevronDown reorder controls (sort order is
  recomputed on save). New i18n keys
  executiveMeetings.manage.attendees.{moveUp,moveDown}.
- Manage tab: per-row Copy button opens a new "Duplicate to date" dialog
  that calls POST /executive-meetings/:id/duplicate with a target date
  picker; on success it switches the day view to the chosen date. New
  i18n keys executiveMeetings.manage.{duplicate, duplicateToDate,
  duplicated, duplicateFailed}.
- Print page (executive-meetings-print.tsx) now uses react-i18next with
  new locale block executiveMeetings.print.* (AR + EN parity), forces
  i18n.changeLanguage(lang) so ?lang=en always prints English. Attendees
  cell in the print table is now textAlign: center (matches the Step 1
  schedule).
- Font settings tightened to spec: families = system/Cairo/Tajawal/
  Noto Naskh Arabic/Amiri (dropped Inter, monospace), weights =
  regular/bold (dropped medium, semibold), alignment = start/center
  (dropped end), size range = 12–22 (was 10–32). Enforced server-side
  in fontSettingsSchema (Zod) and mirrored in the UI selects/range.
  Verified: PATCH font-settings returns 200 for valid combo, 400 for
  fontWeight="medium", 400 for fontSize=30.

Validation-round-3 fixes:
- Tasks RBAC tightened end-to-end: new TASK_VIEW_ROLES = admin +
  office_manager + coord_lead + coordinator. GET /tasks and
  PATCH /tasks/:id now require requireTaskView. /me exposes new
  canViewTasks flag. Frontend isSectionVisible("tasks") gated on
  canViewTasks (so CEO/viewer never sees the Tasks tab or hits the
  endpoint). Verified: admin /me returns canViewTasks=true.
- GET /requests gained dateFrom + dateTo (createdAt range) and a
  requester filter that is honored only for approver/audit roles
  (non-approvers stay locked to their own requests). Verified curl
  with ?dateFrom=...&dateTo=... returns 200.
- Task dueAt schema accepts BOTH YYYY-MM-DD (from <input type="date">)
  and full ISO datetime; date-only is normalized to start-of-day UTC,
  empty string clears the field. Verified curl POST /tasks with
  {"dueAt":"2026-12-31"} returns 201 with stored
  dueAt = 2026-12-31T00:00:00.000Z.

Validation-round-2 fixes:
- GET /executive-meetings/requests: added server-side least-privilege so
  only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every
  other executive role is force-scoped to requestedBy = self regardless
  of the client's `mine` flag (closes the RBAC overexposure flag).
- isSectionVisible("tasks") now also returns true for canSubmitRequest,
  so executive_coordinator sees the Tasks tab (coordinators execute the
  follow-ups generated from approved requests).
- Requests "new request" dropdown now exposes ALL twelve backend request
  types: create, edit, delete, reschedule, add_attendee, remove_attendee,
  change_location, cancel_meeting, note, highlight, unhighlight, other.
- i18n parity: added en.json + ar.json keys for the five missing request
  types (add_attendee, remove_attendee, change_location, cancel_meeting,
  note) and the two missing statuses (needs_edit, done) under
  executiveMeetings.requests.{type,status}.
- Fixed wrong key reference: reschedule form's date row was using
  manage.field.date (does not exist) — switched to manage.field.meetingDate.

Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
  reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync,
  expanded seed data including executive_meeting_notifications sample
  rows.
2026-04-28 08:02:41 +00:00