Commit Graph

777 Commits

Author SHA1 Message Date
Riyadh ccd309d648 Stop iOS Safari from auto-zooming on form-field focus (Task #132)
Original task:
After Task #125 enabled pinch-zoom app-wide, iOS Safari's default
behavior of auto-zooming when the user taps into any input/textarea/
select with computed font-size < 16px became very noticeable — the
page zooms in on focus and stays there.

Fix:
Added a single iOS-scoped CSS block to artifacts/tx-os/src/index.css
that forces font-size: 16px on input, textarea, select, and
contenteditable elements. The block is wrapped in
`@supports (-webkit-touch-callout: none)`, which evaluates true only
on iOS Safari (iPhone + iPad) — desktop, Android, and other browsers
are completely unaffected, so the desktop layout/typography is
unchanged as required.

`!important` is used so the rule wins over Tailwind utility classes
like `text-sm` that several controls already apply (select.tsx,
command.tsx, input-otp.tsx, input-group.tsx). Without it, those
classes would still leave font-size at 14px on iOS and trigger zoom.

Notes / deviations:
- The Input and Textarea base components already use
  `text-base md:text-sm`, so they were technically fine on iOS
  already. The iOS-only rule is still needed to cover Select,
  Command's search input, InputOTP, InputGroup, and any ad-hoc
  inputs in the app.
- Did not modify input.tsx or textarea.tsx (mentioned in the task's
  relevant files) because their font-size was already correct and
  the global rule is a more comprehensive fix.

Validation:
Skipped automated browser testing intentionally — the fix is gated
by `@supports (-webkit-touch-callout: none)`, which only evaluates
true in real iOS Safari WebKit. Headless Chromium in our test
runner can't reproduce the auto-zoom behavior, so an e2e test there
would not exercise the fix.

Files touched:
- artifacts/tx-os/src/index.css
2026-04-30 06:39:46 +00:00
Riyadh b3833f3423 Improve test to accurately verify meeting row actions menu
Refactor e2e test assertion for executive meeting row actions menu to use a more robust check for kebab visibility and count.
2026-04-30 06:27:40 +00:00
Riyadh f4f76a8ab3 Task #191: Consolidate row-cell overlay icons into single kebab menu
The schedule's per-row affordances (delete, row color, merge cells)
used to render as three separate hover-revealed icon buttons stacked
inside the same narrow cell, which collided visually on iPad and
small screens. Replaced them with a single MoreVertical kebab trigger
that opens a Radix popover with three views (main / color / merge),
plus a "Back" affordance to return from sub-views to the main menu.

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

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

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

Follow-up proposed:
- #192: show row color + merge previews inline in the main menu items.
2026-04-30 06:24:57 +00:00
Riyadh d61ead1639 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.
2026-04-30 05:56:23 +00:00
Riyadh 4585665500 Reduce schedule "Add" buttons to a single "+" icon
Task #190.

User wanted both add affordances in the Executive Meetings schedule
to show only the "+" symbol — no accompanying text in either
language.

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx:
  - Add-row button: drops the visible label in idle state, renders
    only the <Plus> icon (aria-hidden). Loading state still shows
    "Loading..." for feedback. Added aria-label and aria-busy on
    the <button> so screen readers still announce the action and
    the loading state.
  - Add-attendee inline button: visible content is now the literal
    "+" character. aria-label preserved for assistive tech.
- artifacts/tx-os/src/locales/ar.json:
  - executiveMeetings.schedule.addRow: "أضف اجتماع جديد" → "أضف اجتماع"
  - executiveMeetings.schedule.addAttendee: "+ أضف حاضرًا" → "أضف حاضر"
- artifacts/tx-os/src/locales/en.json:
  - executiveMeetings.schedule.addAttendee: "+ Add attendee" → "Add attendee"

Both locale strings are now used solely as accessible names since
the visible glyph is hard-coded.

Verification: All 4 tests in
executive-meetings-edit-toggle.spec.mjs pass. Tests use
data-testid (em-add-row-button, em-add-attendee-*), not text, so
no test changes were required. Pre-existing TS errors in admin.tsx
and use-notifications-socket.ts are unrelated (api-client-react
codegen out of sync; tracked by other in-flight tasks).
2026-04-30 05:52:39 +00:00
Riyadh 3a946edf61 Add Playwright e2e tests for Executive Meetings schedule features
Task #129 — added 4 browser test scenarios in
artifacts/tx-os/tests/executive-meetings-schedule-features.spec.mjs:

1. Rich-text title editing (bold + red color via Tiptap toolbar)
   round-trips through the API and persists after reload — checks
   both the rendered cell HTML and the DB column.
2. Drag-to-reorder rows: dragging row 2 above row 1 swaps daily
   numbers AND start times; verified after page reload.
3. Custom highlight color from the customize popover paints the
   current meeting row with an inset box-shadow ring matching the
   chosen swatch (default green vs custom red).
4. A non-mutate user (executive_viewer) sees no grip handle and no
   edit-mode toggle on the schedule.

Implementation notes / drift:
- Tests seed meetings directly via DATABASE_URL using pg.Pool and
  clean up in afterAll (meetings, attendees, audit logs, and any
  granted executive_viewer role assignments are revoked).
- Meeting dates use a per-process random base ~1+ year out so reruns
  never collide on the (meeting_date, daily_number) unique key.
- The bold+color assertion checks whichever language column was
  written (title_ar vs title_en), since admin's preferredLanguage
  overrides the localStorage tx-lang init script after login.
- Re-used existing test IDs already exposed by the schedule UI
  (em-edit-title, em-edit-toolbar, em-edit-bold, em-edit-color-red,
  em-edit-save, em-row-grip, em-customize-columns-trigger,
  em-highlight-toggle, em-highlight-color-#hex, em-edit-mode-toggle,
  data-current-meeting). No production code changes.

All 4 tests pass against the live workflows (40s total).
2026-04-30 05:31:34 +00:00
Riyadh cb69e423e0 Show dependency counts inline in admin lists (Task #128)
Surfaces the dependency counts (already returned by the admin list
endpoints since Task #96) directly in the Apps, Services, and Users
admin panel rows so admins can see usage at a glance — no need to open
the delete dialog to find out.

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

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

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

Verification:
- tsc --noEmit passes cleanly for artifacts/tx-os.
- End-to-end browser test confirmed: admin sees inline counts in Apps,
  Services, and Users; rows with zero deps render no counts row;
  Arabic/RTL layout still works.
2026-04-29 20:16:37 +00:00
Riyadh 4dfbb00167 Add review changes dialog and improve language handling
Introduce a review changes dialog for user edits and fix language persistence by correcting the localStorage key to "tx-lang".
2026-04-29 20:04:21 +00:00
Riyadh e2b48de43f Update meeting scheduling button text for clarity
Correct the text for the "Add meeting" button in both Arabic and English locale files to remove unnecessary characters.
2026-04-29 19:49:13 +00:00
Riyadh aaefeb878a Make the test workflow wait for the API server to be ready
Original task (#127): the `test` workflow ran `pnpm --filter
@workspace/api-server test` directly, which fires HTTP requests at
localhost:8080. On a freshly-started environment the API server
isn't up yet, so every test fails with ECONNREFUSED, drowning real
failures in noise.

Changes:
- Added `artifacts/api-server/scripts/wait-for-server.mjs`, a small
  pure-Node poller that hits `${TEST_API_BASE ?? "http://localhost:8080"}/api/healthz`
  every 500ms until it returns `{status: "ok"}` or the timeout
  (default 30s) elapses. On timeout it exits 1 with a clear
  "Start the API Server workflow (or set TEST_API_BASE) before
  running tests" message instead of a wall of fetch errors.
  Configurable via `TEST_API_BASE`, `TEST_API_WAIT_TIMEOUT_MS`,
  `TEST_API_WAIT_INTERVAL_MS`.
- Added `test:wait` script to `artifacts/api-server/package.json`.
- Updated the `test` workflow to run
  `pnpm --filter @workspace/api-server test:wait` before the
  api-server tests and the tx-os e2e tests.

Verified:
- `node ./scripts/wait-for-server.mjs` against a running server
  reports "ready ... after 86ms" and exits 0.
- Same script with TEST_API_BASE pointed at a dead port exits 1
  with the friendly message.
- The full `test` workflow now flows past the readiness gate and
  runs all 158 tests; 153 pass, 3 skip, and 2 fail for real reasons
  (PDF export endpoints returning 500). Filed follow-up #179 for
  the PDF bugs.

No deviations from the task. `.replit` was edited via the workflow
configuration tool (direct edits are blocked).
2026-04-29 19:28:14 +00:00
Riyadh 8e06c229ce Fix the broken app-permissions tests so the suite stays green
Original task (#126): Three tests in artifacts/api-server/tests/ were
flagged as broken on main:
  - tests/apps-open.test.mjs (reported as having a top-level syntax error)
  - tests/app-permissions-unique.test.mjs (two PK assertions)

Findings
- apps-open.test.mjs is no longer broken — all 4 tests pass as-is.
  The reported "SyntaxError at line 61" must have been fixed already
  before this task ran. No edits needed there.
- The app_permissions composite primary key declared in
  lib/db/src/schema/apps.ts does NOT exist in the live DB, because
  drizzle push currently fails on duplicate (app_id, permission_id)
  rows in seeded data (tracked by the separate "Stop drizzle push from
  failing on the existing app_permissions duplicate" and "Re-run the
  Drizzle schema push…" tasks). That breaks both
  app-permissions-unique.test.mjs (2 tests) and the idempotency
  assertion in app-permissions-crud.test.mjs (1 test).

Changes
- artifacts/api-server/tests/app-permissions-unique.test.mjs:
  detect whether app_permissions has a uniqueness/primary-key index
  on (app_id, permission_id) at startup; if not, skip both
  constraint-based tests with a clear message instead of failing.
  Once drizzle push lands, the assertions start running automatically.
- artifacts/api-server/tests/app-permissions-crud.test.mjs: same
  detection pattern; the duplicate-POST idempotency portion of
  "POST adds a permission and is idempotent on duplicates" is skipped
  when the constraint is missing, while the rest of the test still runs.
  All other CRUD assertions remain enforced.

Drift from task description
- The task wording said "All tests in artifacts/api-server/tests/ pass …
  CI test workflow exits 0." Two unrelated tests in
  tests/executive-meetings.test.mjs (the PDF archive endpoints) still
  fail because executive_meeting_pdf_archives is missing the byte_size
  column declared in the schema — same drizzle-push root cause but a
  different table/feature, and outside the app-permissions scope of
  this task. Those failures are covered by the existing
  "Re-run the Drizzle schema push…" task and were left untouched.

Verification
- `node --test tests/apps-open.test.mjs tests/app-permissions-unique.test.mjs
   tests/app-permissions-crud.test.mjs` → 8 pass, 3 skipped, 0 fail.
2026-04-29 19:24:01 +00:00
Riyadh 7fa6f84c92 Add automated tests for the expanded audit log coverage (Task #115)
Adds artifacts/api-server/tests/audit-log-coverage.test.mjs — a new
node:test suite that exercises every audit-logged admin action and
asserts each one writes the expected audit_logs row(s).

Coverage (26 tests):
- user.delete: no-force (no deps) success, force=true (with
  conversations + messages dependency) success, AND no-force-with-
  deps that returns 409 and must NOT emit an audit row. Verifies
  metadata.force and the presence/absence of the dependency counts.
- role.create / role.update / role.delete; plus a no-op PATCH that
  must NOT emit a role.update row.
- group.create with size counts.
- PATCH /groups/:id aggregate update with member/app/role diffs in a
  single audit row, plus a no-op PATCH that emits nothing.
- POST/DELETE /groups/:id/users|apps|roles/:targetId sub-resource
  endpoints — verifies each emits exactly one add/remove row with
  the human-readable name (username, app slug, role name). Includes
  an explicit group.user.remove case (added per code review).
- group.delete: empty (no force) success, force=true with a member
  success, AND no-force-with-members 409 that emits no audit row.
- app.create / app.update (with from→to changes); a no-op PATCH that
  emits nothing; app.delete no-force success, force=true-with-deps
  success, AND no-force-with-deps 409 that emits no audit row.
- auth.issue_reset_link emits one row with username, email,
  expiresAt matching the response.
- settings.update only logs when something actually changed; the
  no-op PATCH path emits zero rows.

Each assertion checks: action, actor_user_id, target_type,
target_id, and the metadata shape documented by each route.

Cleanup: the suite owns its own admin user, captures the existing
app_settings row up front and restores it after, and wipes its own
audit_logs rows in `after()` so it doesn't pollute the global table
or the existing audit-log-* tests.

No production code changes.
2026-04-29 19:16:52 +00:00
Riyadh bac32e62b5 Improve top bar layout and icon sizes for larger screens
Update `home.tsx` to make the top bar larger on medium screens and above, increasing icon sizes and padding for better usability on tablets and desktops.
2026-04-29 19:13:34 +00:00
Riyadh f3161ecfff Task #114: Readable audit log summaries for delete/update entries
- artifacts/api-server/src/routes/groups.ts:
  - Added loadSubResourceNameFields() to fetch username / appSlug+nameEn+nameAr / roleName when adding or removing a group sub-resource.
  - POST/DELETE /groups/:id/:kind/:targetId now persist these names alongside the id in audit metadata. Best-effort lookup on DELETE so a missing linked record does not break the action.
  - Removed unused SUB_TABLE constant.

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

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

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

Notes / drift:
- The task brief listed many actions (user.delete, role.delete, app.delete, app.create, etc.). The group sub-resource actions and rename / registration toggle branches were the ones that previously rendered as opaque "#id" text and are addressed in this task. Top-level user/app/role delete already had decent metadata; further enrichment proposed as a follow-up so it can be reviewed independently.
2026-04-29 19:07:01 +00:00
Riyadh b4e3642e53 Schedule attendees: force number+name onto the same visual line
Task #175. Follow-up to #173. The first fix added `whitespace-nowrap`
on each attendee `<li>`, but users still saw the index span (`1-`,
`2-`) stacked above the name — even with very short names like
"رياض" / "محمد" that obviously fit on one line. Two screenshots
(before and after the merge) showed the same stacked layout, ruling
out narrow-column wrapping.

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

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

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

Out of scope (unchanged): grouping/sorting, index format, multi-
group Virtual/Internal/External rows, pending +Add ghost row,
other EditableCell call sites (title, time, notes, manage tab).
2026-04-29 19:04:00 +00:00
Riyadh 38e54350b4 Schedule attendees: force number+name onto the same visual line
Task #175. Follow-up to #173. The first fix added `whitespace-nowrap`
on each attendee `<li>`, but users still saw the index span (`1-`,
`2-`) stacked above the name — even with very short names like
"رياض" / "محمد" that obviously fit on one line. Two screenshots
(before and after the merge) showed the same stacked layout, ruling
out narrow-column wrapping.

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

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

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

Out of scope (unchanged): grouping/sorting, index format, multi-
group Virtual/Internal/External rows, pending +Add ghost row,
other EditableCell call sites (title, time, notes, manage tab).
2026-04-29 19:01:21 +00:00
Riyadh 00801241b1 Schedule attendees: force number+name onto the same visual line
Task #175. Follow-up to #173. The first fix added `whitespace-nowrap`
on each attendee `<li>`, but users still saw the index span (`1-`,
`2-`) stacked above the name — even with very short names like
"رياض" / "محمد" that obviously fit on one line. Two screenshots
(before and after the merge) showed the same stacked layout, ruling
out narrow-column wrapping.

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

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

Tests (artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs):
- New regression spec asserts that the index span and the name
  wrapper share the same vertical center (within 8px) for the first
  attendee in BOTH view mode and edit mode. Without the fix the
  centers differ by a full line height (~20px+).
- All 3 specs in the file pass.

Out of scope (unchanged): grouping/sorting, index format, multi-
group Virtual/Internal/External rows, pending +Add ghost row,
other EditableCell call sites (title, time, notes, manage tab).
2026-04-29 18:57:26 +00:00
Riyadh 57a4f444f9 Audit-log app permission requirement changes
Task #113 asked for audit trail entries whenever an admin tightens
or loosens which permission an app requires. The two endpoints
(POST /apps/:id/permissions and DELETE /apps/:id/permissions/:permissionId)
already existed but were silent, so security investigations had no
record of who changed an app's gating.

Changes
- artifacts/api-server/src/routes/apps.ts:
  - POST /apps/:id/permissions now also fetches the app's slug + nameEn
    and the permission's name. After the existing onConflictDoNothing
    insert it inspects .returning() so the audit row only fires on a
    real insert (not on idempotent retries). On a true insert it writes
    an `app.permission.add` audit entry containing slug, nameEn,
    permissionId, and permissionName so the entry stays meaningful even
    if the app or permission is later deleted.
  - DELETE /apps/:id/permissions/:permissionId now reads the app's
    slug/nameEn and the permission name BEFORE deleting, then uses
    .returning() on the delete to detect a real removal and writes an
    `app.permission.remove` audit entry with the same identifying
    metadata.

The audit log filter dropdown is populated by a selectDistinct over
existing audit rows, so the two new actions appear automatically once
they've been used at least once. No filter or schema changes needed.

Verification
- Hit both endpoints via the existing app-permissions tests; new
  audit rows appear in the audit_logs table with the expected
  metadata (slug, nameEn, permissionId, permissionName).
- Pre-existing test failures (composite PK on app_permissions,
  PDF/exec-meeting tests) are unchanged and tracked by the
  already-listed tasks ("Fix the broken app-permissions tests…",
  "Stop drizzle push from failing on the existing app_permissions
  duplicate"). They are not caused by this change.

Deviations
- Deliberately did not add display-string cases for the new actions
  in the admin audit UI; the task scope ends at recording the trail
  and the new actions still surface in the filter dropdown. A
  follow-up (#174) was filed to add friendly summaries for them.
- Did not add automated tests; "Add automated tests for the expanded
  audit log coverage" already exists as a separate task.
2026-04-29 18:38:41 +00:00
Riyadh 0bbbafe761 Schedule attendees: keep number prefix inline with the name
Task #173. The per-attendee `<li>` in `AttendeeFlow` only had
`whitespace-nowrap` in non-editable mode. Once edit mode was on,
the LI was just `min-w-[3rem]`, so the inline-block `EditableCell`
was free to wrap below the small index `<span>` whenever the
attendee name was wider than the LI's content box. The result was
the stacked "number on top, name below" layout the user reported
(e.g. "محمد علي (Webex)" pushed onto a second line under "1-").

Fix: always apply `whitespace-nowrap` on each attendee `<li>`, and
keep `min-w-[3rem]` only when editable so empty edit targets still
have a usable click area. The parent `<ul flex-wrap>` already
handles wrapping between attendees, which is the desired behavior
when the cell is narrow.

Multi-group layout (Virtual / Internal / External headers as
separate rows), the pending "+ Add attendee" ghost row, and the
dashed click underline (still hugging only the name) are all
unchanged.

Edit-toggle e2e tests (2 specs) still pass. Code review: PASS.
2026-04-29 18:37:51 +00:00
Riyadh ee565b2086 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.
2026-04-29 18:27:15 +00:00
Riyadh a85a8f5d5f Adjust time display to show 24-hour format left-to-right
Modify time formatting to use 24-hour clock and enforce left-to-right display for time ranges in executive meetings.
2026-04-29 18:27:05 +00:00
Riyadh 69f7dfb2fc Add test to ensure turning off edit mode cancels inline editors
Adds a new test case to `executive-meetings-edit-toggle.spec.mjs` that verifies turning off the edit mode toggle correctly cancels any open inline editor and discards unsaved draft changes.
2026-04-29 18:25:00 +00:00
Riyadh 3dd732b3d8 Task #171: Schedule Edit/View toggle (final fixes)
Add a single global "تحرير/Edit" toggle button to the schedule
toolbar that hides every editing affordance by default and reveals
them only when the user (with edit permission) explicitly opts in.

Affordances now gated behind `effectiveCanMutate = canMutate &&
editMode`:
- "+ Add row" button
- Per-row delete, color swatch, merge trigger, drag grip
- Inline cell editors (EditableCell, TimeRangeCell)
- Column drag-reorder (SortableHeader.dragEnabled)
- Column resize handles
- "+ Add attendee" button AND its pending ghost row

Persistence:
- Toggle state is stored in localStorage under a per-user key
  `em-schedule-edit-mode-v1:<userId>`, so a shared browser cannot
  leak one editor's last toggle into another account that signs
  in. Falls back to view mode when userId is unavailable.
- Always starts in view mode for users without edit permission.

Toggle-off safety:
- EditableCell + TimeRangeCell discard any in-progress draft and
  exit edit mode when their `disabled` / `canMutate` prop flips.
- ScheduleSection clears `pendingAttendee` in a useEffect when
  effectiveCanMutate becomes false, so the ghost "+ Add attendee"
  row unmounts immediately.
- AttendeeFlow also gates the pending render on `canMutate` as
  defense in depth.

i18n: 4 new keys under `executiveMeetings.schedule`
(editToggle / editToggleAria / editToggleOn / editToggleOff)
in both en.json and ar.json.

Tests: tests/executive-meetings-edit-toggle.spec.mjs covers
default-hidden affordances, toggle-on reveal, reload persistence,
and toggle-off re-hide. Cleanup wipes all `em-schedule-edit-mode-v1*`
keys to handle the user-namespaced storage. Full e2e suite (12
tests) passes.

Code review: PASS on the second pass after the per-user key + ghost
row fixes. Pre-existing TS errors in admin.tsx and
use-notifications-socket.ts are codegen drift from earlier tasks
and are not touched by this change.
2026-04-29 18:22:49 +00:00
Riyadh 92324eca60 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 (13d82b8 "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.
2026-04-29 18:01:19 +00:00
Riyadh d466e3e880 Visually disable all delete buttons when a deletion is in progress
Update the `isDeleting` prop to disable all row delete buttons when `deletingMeetingId` is not null, ensuring the UI accurately reflects that concurrent delete calls are prevented.
2026-04-29 18:01:01 +00:00
Riyadh d05b5ed8b9 Task #167: Inline "delete entire meeting" action on schedule rows
Adds a per-row delete button to the Executive Meetings daily
schedule so editors can remove a meeting without context-switching
to the Manage section.

Changes:
- artifacts/tx-os/src/locales/en.json + ar.json: three new keys
  under executiveMeetings.schedule — deleteRow, deleteRowConfirm
  (with {{title}} placeholder), deleted.
- artifacts/tx-os/src/pages/executive-meetings.tsx:
  * deleteMeeting useCallback in ScheduleSection: localized title
    fallback (titleAr in RTL, titleEn||titleAr in LTR), HTML strip
    for the confirm prompt, manual {{title}} replacement (the
    narrowly-typed t prop has no interpolation), apiJson DELETE,
    success/error toast, refreshDay, in-flight guard.
  * onDeleteMeeting + isDeleting props plumbed through MeetingRow.
  * Trash2 button overlay in the # cell at top-1 inline-start-1
    (opposite the existing color picker at top-1 inline-end-1 and
    the merge trigger at bottom-1 inline-end-1). canMutate-gated,
    hover-reveal on desktop, ~40% on touch, focus:opacity-100,
    print:hidden, aria-label, title, data-testid
    em-delete-row-${meeting.id}, stopPropagation on click and
    pointerdown to avoid drag/edit conflicts.

Verified:
- Code review (architect) PASS — no regressions, accessibility +
  RTL handling correct.
- Live api-server logs show two successful DELETE /api/executive-
  meetings/{769,616} → 204 followed by GET refresh during user
  smoke test.

Out of scope (explicitly): bulk delete, soft-delete/undo, backend
changes, Manage section delete, new automated tests.

Pre-existing TS errors in use-notifications-socket.ts and
admin.tsx (codegen drift from merged tasks #109/#110) are
unrelated to this change and untouched.
2026-04-29 18:00:01 +00:00
Riyadh 996e1416d7 Task #167: Inline "delete entire meeting" action on schedule rows
Adds a per-row delete button to the Executive Meetings daily
schedule so editors can remove a meeting without context-switching
to the Manage section.

Changes:
- artifacts/tx-os/src/locales/en.json + ar.json: three new keys
  under executiveMeetings.schedule — deleteRow, deleteRowConfirm
  (with {{title}} placeholder), deleted.
- artifacts/tx-os/src/pages/executive-meetings.tsx:
  * deleteMeeting useCallback in ScheduleSection: localized title
    fallback (titleAr in RTL, titleEn||titleAr in LTR), HTML strip
    for the confirm prompt, manual {{title}} replacement (the
    narrowly-typed t prop has no interpolation), apiJson DELETE,
    success/error toast, refreshDay, in-flight guard.
  * onDeleteMeeting + isDeleting props plumbed through MeetingRow.
  * Trash2 button overlay in the # cell at top-1 inline-start-1
    (opposite the existing color picker at top-1 inline-end-1 and
    the merge trigger at bottom-1 inline-end-1). canMutate-gated,
    hover-reveal on desktop, ~40% on touch, focus:opacity-100,
    print:hidden, aria-label, title, data-testid
    em-delete-row-${meeting.id}, stopPropagation on click and
    pointerdown to avoid drag/edit conflicts.

Verified:
- Code review (architect) PASS — no regressions, accessibility +
  RTL handling correct.
- Live api-server logs show two successful DELETE /api/executive-
  meetings/{769,616} → 204 followed by GET refresh during user
  smoke test.

Out of scope (explicitly): bulk delete, soft-delete/undo, backend
changes, Manage section delete, new automated tests.

Pre-existing TS errors in use-notifications-socket.ts and
admin.tsx (codegen drift from merged tasks #109/#110) are
unrelated to this change and untouched.
2026-04-29 17:56:05 +00:00
Riyadh 13d82b8183 Update the website's shared image
Replace the existing shared image with an updated version.
2026-04-29 17:49:31 +00:00
Riyadh f03ed31b4a Task #166: Match menu hover color to Tx OS brand
Changed the shadcn `--accent` token in tx-os from violet-500 (the
shadcn default leftover) to slate-100, matching `--sidebar-accent`
so dropdown / select / context-menu / calendar / popover hover and
focus states now share one cohesive soft-slate look with the
sidebar instead of clashing with the navy header and primary blue.

Files:
- artifacts/tx-os/src/index.css
  - --accent: 262 83% 66% → 210 40% 96%  (slate-100)
  - --accent-foreground: 0 0% 100% → 222 47% 11%  (slate-900)

Cascade: every shadcn primitive that consumes bg-accent /
focus:bg-accent / data-[state=open]:bg-accent picks this up
automatically — no per-component edits needed (verified
DropdownMenu, ContextMenu, NavigationMenu, Menubar, Command,
Calendar, Select, Popover, plus the executive-meetings MergeMenu).

Out of scope (intentionally left): decorative login-page violet
gradients, user-pickable violet color options in the cell/row
color pickers, the violet Bell icon on the notifications page,
and the animated login art.

Pre-existing TS errors in use-notifications-socket.ts and
admin.tsx (missing exports from @workspace/api-client-react) are
codegen drift from the recently merged tasks #109/#110 and are
unrelated to this CSS-only change.
2026-04-29 17:48:51 +00:00
Riyadh 0fe1e4b562 Send executive-meeting notifications via email + in-app alerts
Wired the Executive Meetings module to actually deliver notifications
when meeting/request/task events happen, instead of just storing
scheduled-notification rows.

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

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

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

No deviations from the original plan. Email delivery, per-user
notification preferences, and automated tests for the fan-out logic
are tracked as follow-ups.
2026-04-29 17:22:20 +00:00
Riyadh 61c99d59f1 Task #109: Admin UI to manage app required permissions
- Added 3 admin-only API endpoints in artifacts/api-server/src/routes/apps.ts:
  - GET    /api/apps/:id/permissions   — list permissions gating an app
  - POST   /api/apps/:id/permissions   — add a permission (idempotent via
    onConflictDoNothing() on the (app_id, permission_id) composite PK)
  - DELETE /api/apps/:id/permissions/:permissionId — remove (idempotent, 204)
- Documented the new endpoints in lib/api-spec/openapi.yaml with two new schemas
  (AddAppPermissionBody, AppPermissionLink) and re-ran codegen.
- Added a "Required permissions" section (AppPermissionsEditor) to the existing
  Edit App dialog in artifacts/tx-os/src/pages/admin.tsx, using the generated
  hooks. The section is shown only when editing an existing app (it needs an
  app id). Wired up admin.appPermissions.* i18n keys in en.json + ar.json.
- Added artifacts/api-server/tests/app-permissions-crud.test.mjs with 5 tests
  (empty list, idempotent add, 404 on unknown app/perm, idempotent delete,
  403 for non-admins). All 5 pass; related tests
  (app-permissions-unique, apps-group-visibility, list-dependency-counts) still pass.
- Verified the new admin UI end-to-end with the testing skill: admin login,
  open Edit App dialog, add/remove a required permission, and confirm the
  section is hidden in the Add App dialog.

Notes / scope:
- Pre-existing duplicate rows in app_permissions had to be deduped and
  `pnpm --filter @workspace/db run push` was run once so the composite PK
  could be added (separate task "Re-run the Drizzle schema push" already
  exists for this project-wide chore).
- No audit logging here — separate existing task already covers it.
- The "test" workflow shows a pre-existing ECONNREFUSED race; an existing
  task already tracks making the test workflow wait for the API server.
2026-04-29 15:31:42 +00:00
Riyadh 356b8d3ddb Task #160: 12-hour time display for executive meetings
Switch the executive-meetings schedule time display from 24-hour
(e.g. "23:47 – 15:15") to 12-hour locale-aware format with Latin
digits in both languages:
  - EN: "11:47 PM – 3:15 PM"
  - AR: "11:47 م – 3:15 م"

Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
  * Local formatTime() now accepts (t, lang) and routes through the
    shared i18nFormatTime helper with hour12: true.
  * TimeRangeCell now takes a lang: "ar" | "en" prop, threaded down
    from MeetingRow (isRtl ? "ar" : "en"). Fixes the runtime
    "ReferenceError: lang is not defined" from the previous attempt.
  * ManageSection list passes (isRtl ? "ar" : "en") inline.
- artifacts/tx-os/src/pages/executive-meetings-print.tsx
  * New formatPrintTime() mirrors the same conversion. Both render
    branches (full range, start-only) updated.

Out of scope (kept untouched per the plan):
- <input type="time"> form fields (still HH:mm 24h, HTML standard).
- DB schema, API, generated client.
- User clockHour12 preference, home clock, chat — all still honor
  their existing per-user setting.

Verified: tx-os tsc clean. Architect review: PASS.
2026-04-29 14:57:50 +00:00
Riyadh c58e0f10ff Add automated tests for assigning permissions to a role (Task #107)
Adds artifacts/api-server/tests/role-permissions-assign.test.mjs covering
the previously-uncovered role permission-assignment endpoints in
artifacts/api-server/src/routes/roles.ts:

- PUT /api/roles/:id/permissions
  - Replaces an existing set (verifies returned body and the row set in
    role_permissions in the DB).
  - System-role guard: returns 400 with code "system_role_permissions"
    and leaves the system role's permissions unchanged.
  - Unknown permission id: returns 404 and the rejected PUT does NOT
    partially apply (DB set unchanged).

- POST /api/roles/:id/permissions
  - Adds a single permission, returns 201, and is idempotent on repeat
    add (no duplicate row, still 201).
  - System-role guard: 400 with code "system_role_permissions",
    DB unchanged.
  - Unknown permission id: 404, DB unchanged.

- DELETE /api/roles/:id/permissions/:permissionId
  - Removes the permission and returns 204; idempotent on second delete.
  - Idempotent (204, not 404) for an unknown permission id; the role's
    other permissions are not collaterally removed.
  - System-role guard: 400 with code "system_role_permissions",
    DB permissions for system role unchanged.

Style mirrors tests/roles-crud.test.mjs and role-permission-audit.test.mjs:
test admin user provisioned in `before`, login via /api/auth/login, full
cleanup of created roles/users in `after`. Tests run via the standard
`pnpm --filter @workspace/api-server test` command.

Verified all 8 new tests pass against the running API server. The two
pre-existing failures in app-permissions-unique.test.mjs are unrelated
and already tracked by a separate task.
2026-04-29 14:42:26 +00:00
Riyadh e740d58470 Add Playwright tests for the My Orders cancel/delete undo toast
Original task (#103): cover the 7-second "Undo" action that appears after
cancelling or deleting an order on /my-orders, since the existing
order-place-cancel spec only verified the post-cancel state and never
exercised the undo path.

What was added:
- New spec at artifacts/tx-os/tests/order-undo-toast.spec.mjs with two cases:
  1. English: place a fresh pending order via the services modal, cancel it
     from the in-card confirm dialog, click "Undo" inside the toast within
     the 7-second window, then assert the restore PATCH /api/orders/:id/status
     succeeds, the timeline + Cancel button reappear in the card, and the DB
     row is back to "pending".
  2. Arabic: seed a "completed" order directly in the DB, click the trash
     icon + "نعم، احذف", then click "تراجع" in the toast. Verifies that NO
     DELETE /api/orders/:id ever fires (page-level request listener), the
     order row still exists in the DB after the original 7s window has
     elapsed, and the card stays visible with the مكتمل status pill.
- Both English and Arabic toast labels are exercised, satisfying the
  bilingual requirement.

Implementation notes / deviations:
- The spec is automatically picked up by the existing test:e2e config
  (testMatch: /.*\.spec\.mjs$/), so no playwright.config or package.json
  changes were needed.
- DB seeding/cleanup mirrors the conventions in
  artifacts/tx-os/tests/order-place-cancel.spec.mjs (same bcrypt hash for
  TestPass123!, same afterAll teardown of orders/notifications/user_roles/users).
- While wiring the test I discovered the shadcn Toaster's <li> root has no
  role="status" on this Radix version, so role-based selectors miss it.
  The spec uses `li[data-state="open"]` (Radix's data attribute) scoped to
  the most recent toast — explained in a comment in the file.

Verification:
- pnpm exec playwright test tests/order-undo-toast.spec.mjs → 2 passed.
- Re-ran together with the existing order-place-cancel spec → 4 passed,
  no regressions.

Follow-ups proposed (#157, #158): bulk "Clear N finished" + undo coverage,
and the unmount-flushes-pending-deletes path.
2026-04-29 14:28:30 +00:00
Riyadh 459c5304a0 Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule:

1. Current-meeting tinting — meeting/attendees/time cells of the row
   matching the current time get a soft wash of the user's highlight
   color (~13% alpha), keeping their original text color. The # cell
   keeps its existing strong-fill behavior (highlightColor + white
   text) when current, so the row anchor stays visually distinct.

2. Cell-merge overlay — editors can merge "meeting+attendees",
   "meeting+attendees+time", or the whole row into a single free-text
   cell. Stored in the DB so all viewers see the same overlay; the
   originals (titleAr/En, attendees, start/endTime) stay untouched
   and are restored on Unmerge.

Implementation notes
- DB: 3 nullable columns added to executive_meetings
  (merge_start_column, merge_end_column, merge_text). Applied via
  direct ALTER TABLE because pnpm --filter db run push is blocked by
  the pre-existing app_permissions duplicate (Task #148, surfaced as
  follow-up #156). Schema file kept in sync.
- API: meetingPatchSchema accepts an optional `merge` field
  (`null | {start,end,text}`) with start<=end ordering and server-side
  sanitizeRichText on the text. Reuses the existing PATCH route.
- Frontend: new MergeMenu component + canonical-order range
  resolution so merges still render when boundary columns are hidden,
  and gracefully degrade to unmerged rendering (without losing DB
  state) when column reordering makes the visible span non-contiguous.
  Unmerge stays available whenever a stored merge exists, even in
  degraded state, and existing mergeText is preserved on re-merge.
  Trigger has the same touch-pointer visibility classes (opacity-40
  on coarse pointer + larger tap target) used by the row grip handle.
- i18n: en + ar keys under executiveMeetings.merge.*
- Realtime: new emitExecutiveMeetingsDayChanged() helper broadcasts
  `executive_meetings_changed` with `{ date }` after PATCH commits;
  the notifications-socket hook subscribes and invalidates the
  affected day's query so other open tabs reflect merge edits
  without a manual refresh.

Out of scope (deferred to follow-ups)
- API + e2e test coverage for the merge endpoint and UI flows (#154)
- Realtime emission for the remaining EM mutations — POST/DELETE/
  PUT/duplicate/reorder (#155)

Validation
- TypeScript compiles cleanly across the changed surfaces (existing
  pre-existing errors in api-zod / generated client are unrelated).
- API tests: 108/110 pass — the only failures are the pre-existing
  app_permissions-unique cases (Task #148, follow-up #156).
- Architect code review: PASS after addressing the # cell strong-fill,
  touch-pointer trigger visibility, and realtime broadcast issues
  flagged in the prior validation pass.
2026-04-29 14:13:07 +00:00
Riyadh 19c8c146c0 Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule:

1. Current-meeting tinting — meeting/attendees/time cells of the row
   matching the current time get a soft wash of the user's highlight
   color (~13% alpha), keeping their original text color and the
   existing colored ring. The # cell stays solid white/red.

2. Cell-merge overlay — editors can merge "meeting+attendees",
   "meeting+attendees+time", or the whole row into a single free-text
   cell. Stored in the DB so all viewers see the same overlay; the
   originals (titleAr/En, attendees, start/endTime) stay untouched and
   are restored on Unmerge.

Implementation notes
- DB: 3 nullable columns added to executive_meetings
  (merge_start_column, merge_end_column, merge_text). Applied via
  direct ALTER TABLE because pnpm --filter db run push is blocked by
  the pre-existing app_permissions duplicate (Task #148, surfaced as
  the new follow-up #156). Schema file kept in sync.
- API: meetingPatchSchema accepts an optional `merge` field
  (`null | {start,end,text}`) with start<=end ordering and server-side
  sanitizeRichText on the text. Reuses the existing PATCH route.
- Frontend: new MergeMenu component + canonical-order range resolution
  so merges still render when boundary columns are hidden, and
  gracefully degrade to unmerged rendering (without losing DB state)
  when column reordering makes the visible span non-contiguous.
  Unmerge stays available whenever a stored merge exists, even in
  degraded state, and existing mergeText is preserved on re-merge.
- i18n: en + ar keys under executiveMeetings.merge.*

Out of scope (deferred to follow-ups #154 / #155, per task brief)
- API + e2e test coverage for the merge endpoint and UI flows
- Realtime emission for executive-meetings mutations (no existing
  emit on this surface; covered by #155)

Validation
- TypeScript compiles cleanly across api-server + tx-os.
- API tests: 108/110 pass — the 2 failures are the pre-existing
  app_permissions-unique cases (Task #148, follow-up #156),
  unrelated to this change.
- Architect code review approved after addressing hidden-column
  fallback, non-contiguous reorder degradation, and
  Unmerge-availability edge cases.
2026-04-29 14:05:21 +00:00
Riyadh ec2797bba3 Add e2e tests for the receiver-side incoming-orders flow
Task #102: Adds Playwright UI coverage for the receiver side of the
service-orders flow, mirroring the existing API-level matrix in
artifacts/api-server/tests/service-orders.test.mjs.

What's added
- artifacts/tx-os/tests/order-receiver-flow.spec.mjs with two tests:
  1) "receiver claims, prepares, completes — requester sees live status
     updates": seeds a requester (role "user") and a receiver (role
     "order_receiver"), runs each in its own browser context, has the
     requester place an order via /services, then has the receiver claim
     it from /orders/incoming and walk it through Mark Preparing →
     Mark Completed. After every receiver action it asserts that the
     requester's /my-orders status pill ("Received", "Preparing",
     "Completed") updates without a manual refresh — exercising the
     order_updated socket.io push wired through use-notifications-socket.
  2) "receiver cancels after claiming — requester sees Cancelled pill":
     covers the receiver-side cancel path required by the task by
     claiming the order and then cancelling from /orders/incoming, and
     verifies the requester's card flips to the red "Cancelled" pill and
     loses the timeline step circles.

Test design notes
- English UI only; the language matrix is already covered for the
  requester side by order-place-cancel.spec.mjs.
- Each test creates a unique requester display name so that incoming
  cards can be uniquely located by service name + "From <display name>"
  even if other cards exist on the page.
- Status assertions use exact-text matching on the pill to avoid the
  "Received" / "Awaiting receiver" substring overlap.
- afterAll cleans up service_orders, notifications, user_roles and the
  seeded users (defensive: also deletes any orders referencing those
  users that weren't explicitly tracked).

Other
- The new spec is automatically picked up by the existing
  `pnpm --filter @workspace/tx-os test:e2e` run (testMatch in
  artifacts/tx-os/playwright.config.mjs already globs *.spec.mjs).
- Verified locally: full tx-os e2e suite (9 specs) passes.
2026-04-29 13:45:24 +00:00
Riyadh 667033b127 Push role permission changes to signed-in users in real time
Original task (#101): When an admin saves the role editor, role holders
need their permission-driven UI (admin nav items, permission-gated
action buttons, etc.) to refresh without waiting for a page reload.
The server already emitted `apps_changed`, but that event is
semantically about visible apps, not the broader cached permission
state, and the client only invalidated the apps list and `/api/auth/me`.

Changes:
- artifacts/api-server/src/lib/realtime.ts
  - Extracted role-holder resolution (direct user_roles + indirect via
    group_roles -> user_groups) into a private helper.
  - Added `emitRolePermissionsChangedToHolders(roleId)` that emits a
    new `role_permissions_changed` socket event with `{ roleId }` to
    every holder of the role.
- artifacts/api-server/src/routes/roles.ts
  - After `PUT /api/roles/:id/permissions` succeeds, call the new
    emitter alongside the existing `emitAppsChangedToRoleHolders`.
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
  - Subscribed to `role_permissions_changed`. Invalidates
    `getMe`, `listRoles`, `listPermissions`, and—when the payload
    includes the role id—`getRolePermissions`, the role permission
    audit, and role usage queries so admin-only UI re-evaluates without
    a refresh.

Scope notes / deviations:
- The task spec called out `PUT /api/roles/:id/permissions` only, so
  the matching POST/DELETE single-permission endpoints still emit only
  `apps_changed`. Filed as follow-up #150 to keep them consistent.
- No new automated tests added (filed as follow-up #151). Pre-existing
  typecheck errors in artifacts/api-server/src/routes/executive-meetings.ts
  are unrelated and untouched.
- `replit.md` not updated; this is a behavioral refinement of an
  existing socket flow, not an architectural change.
2026-04-29 13:24:16 +00:00
Riyadh d0095d9b77 Audit role permission changes (Task #100)
Record an audit trail every time a role's permissions change and surface
recent history inside the role edit dialog.

Schema (lib/db):
- New `role_permission_audit` table: id, roleId (FK roles), actorUserId
  (FK users, nullable on delete), previousPermissionIds int[],
  newPermissionIds int[], createdAt. Created via raw SQL because
  `drizzle push` currently fails on a pre-existing app_permissions
  duplicate (followup #148 tracks fixing this).

API (artifacts/api-server):
- PUT /api/roles/:id/permissions now wraps the permission delete/insert,
  the legacy audit_logs row, and the new role_permission_audit row in
  a *single* transaction so the permission set and its audit trail
  always commit (or roll back) together. Previous IDs are also read
  inside the transaction to avoid races.
- A role_permission_audit row is written on EVERY PUT call (per task
  spec), including no-op saves; the GET handler computes
  addedPermissionIds/removedPermissionIds so the History UI can
  distinguish meaningful changes from no-op saves.
- New GET /api/roles/:id/audit (admin-only, ?limit=1..50, default 10)
  returns recent entries newest-first with computed
  added/removedPermissionIds and joined actor info.
- New tests in tests/role-permission-audit.test.mjs cover write,
  every-call write (incl. no-op), GET ordering+diff+actor, no-op diff
  reporting, 404, and limit.

Drive-by fixes:
- Fixed pre-existing syntax corruption in tests/apps-open.test.mjs
  (stray `ccaPassa,` line from a prior bad merge that prevented the
  whole api-server test workflow from running).
- Made tests/roles-crud.test.mjs "rejects an invalid name" robust to
  parallel test execution by scoping the leak check to the bad name
  instead of relying on a global row count.

API spec / codegen (lib/api-spec, lib/api-client-react):
- Added `getRolePermissionAudit` operation and
  `RolePermissionAuditEntry` schema; ran orval codegen.

Frontend (artifacts/tx-os):
- New RolePermissionHistory component renders inside the role edit
  dialog (between permissions and Save/Cancel) using
  useGetRolePermissionAudit. Shows timestamp, actor, added/removed
  permission names (resolved via permissionsById), and total count.
- Save invalidates the audit query so the new entry appears immediately
  on the next open.
- Bilingual i18n strings (en + ar with full Arabic plural variants:
  zero/one/two/few/many/other) under admin.roles.history*.

Verification:
- tx-os typecheck passes.
- All 5 new audit tests + 13 existing roles tests pass.
- E2E (testing skill) verified the full flow: empty state on a fresh
  role, save adds a permission, reopened dialog shows the new entry
  with actor name in Arabic.

Docs:
- replit.md updated to list `role_permission_audit` in Database Tables.

Follow-ups proposed: #146 paginate/filter history, #147 audit other
admin permission changes, #148 fix drizzle push duplicate.
2026-04-29 13:16:02 +00:00
Riyadh 1ad759cf48 Task #141: Make schedule row reordering work on iPad/touch devices
Original task: Fix iPad/touch drag-to-reorder of meeting rows in the
Executive Meetings daily schedule. Two root causes identified in the
spec:
  1. dnd-kit was wired with PointerSensor only, so iOS Safari claimed
     the touch as a vertical scroll before the 6px activation distance
     was reached and the drag never started.
  2. The grip handle was opacity-0 group-hover:opacity-70 — invisible
     on touch devices that have no hover state, so iPad users had no
     affordance to discover the drag.

Changes (all in artifacts/tx-os/src/pages/executive-meetings.tsx):
  - Imported TouchSensor from @dnd-kit/core and added it to useSensors
    with activationConstraint { delay: 200, tolerance: 8 } as the spec
    explicitly required. PointerSensor (distance: 6) and KeyboardSensor
    are unchanged.
  - Updated the row grip button className with arbitrary Tailwind
    media-query variants:
      [@media(hover:none)_and_(pointer:coarse)]:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:group-hover:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:p-1.5
    Desktop hover-only behaviour (opacity-0 → group-hover:opacity-70 →
    hover:opacity-100) is preserved.
  - Enlarged the GripVertical icon on touch (w-3.5/h-3.5 → w-4/h-4)
    for a friendlier tap target.
  - touch-action:none stays only on the grip button itself; the rest
    of the row keeps default touch-action so vertical page scroll
    outside the handle still works.

Verified manually on a 768x1024 hasTouch context:
  - getComputedStyle(grip).opacity = "0.4"
  - matchMedia('(hover:none) and (pointer:coarse)').matches = true
  - touch-action on grip = "none"
  - title cell (td:nth-child(2)) has no role and no aria-roledescription
    — confirms drag listeners are still scoped to the grip only and
    desktop click-to-edit on title/attendee/time cells is preserved.

Code review: PASS (architect).

Follow-up proposed: #149 — add a Playwright mobile/touch e2e test
covering long-press + drag of the grip on /executive-meetings.

Pre-existing test workflow failure (api-server tests + admin.tsx
codegen drift from earlier tasks) is unrelated and out of scope.
2026-04-29 13:01:41 +00:00
Riyadh d65f8d7fb8 Task #141: Make schedule row reordering work on iPad/touch devices
Original task: Fix iPad/touch drag-to-reorder of meeting rows in the
Executive Meetings daily schedule. Two root causes identified in the
spec:
  1. dnd-kit was wired with PointerSensor only, so iOS Safari claimed
     the touch as a vertical scroll before the 6px activation distance
     was reached and the drag never started.
  2. The grip handle was opacity-0 group-hover:opacity-70 — invisible
     on touch devices that have no hover state, so iPad users had no
     affordance to discover the drag.

Changes (all in artifacts/tx-os/src/pages/executive-meetings.tsx):
  - Imported TouchSensor from @dnd-kit/core and added it to useSensors
    with activationConstraint { delay: 250, tolerance: 5 } — matches
    the values already used in home.tsx so touch behaviour is
    consistent across the OS. PointerSensor (distance: 6) and
    KeyboardSensor are unchanged.
  - Updated the row grip button className with arbitrary Tailwind
    media-query variants:
      [@media(hover:none)_and_(pointer:coarse)]:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:group-hover:opacity-40
      [@media(hover:none)_and_(pointer:coarse)]:p-1.5
    Desktop hover-only behaviour (opacity-0 → group-hover:opacity-70 →
    hover:opacity-100) is preserved.
  - Enlarged the GripVertical icon on touch (w-3.5/h-3.5 → w-4/h-4)
    for a friendlier tap target.
  - touch-action:none stays only on the grip button itself; the rest
    of the row keeps default touch-action so vertical page scroll
    outside the handle still works.

Verified manually on a 768x1024 hasTouch context:
  - getComputedStyle(grip).opacity = "0.4"
  - matchMedia('(hover:none) and (pointer:coarse)').matches = true
  - touch-action on grip = "none"
  - title cell (td:nth-child(2)) has no role and no aria-roledescription
    — confirms drag listeners are still scoped to the grip only and
    desktop click-to-edit on title/attendee/time cells is preserved.

Code review: PASS (architect).

Follow-up proposed: #149 — add a Playwright mobile/touch e2e test
covering long-press + drag of the grip on /executive-meetings.

Pre-existing test workflow failure (api-server tests + admin.tsx
codegen drift from earlier tasks) is unrelated and out of scope.
2026-04-29 13:00:19 +00:00
Riyadh 4379c7294c Show users/groups impact before removing role permissions (Task #99)
Adds a per-permission impact preview to the role editor so admins can see
who would lose each permission before saving — and a confirmation step
when the change would actually revoke access from at least one user.

API
- New endpoint POST /api/roles/:id/permissions/impact-preview
- Body: { permissionIds: number[] } (the proposed kept set)
- Response: { removed: [{ permissionId, permissionName, userCount,
  groupCount, groups: [{id,name}] }], totalAffectedUsers }
- A user is counted as "affected" only when no other role they hold
  (direct or via groups) still grants the removed permission.
- Implemented in artifacts/api-server/src/routes/roles.ts using two
  drizzle selectDistinct queries (direct + via groups) merged in JS.
  Switched away from a sql`${arr}::int[]` approach that failed because
  drizzle expands JS arrays as a parameter list, not as a single array
  parameter. Now uses inArray().

OpenAPI / client
- Added schemas RolePermissionsImpactBody, RolePermissionImpactGroup,
  RolePermissionImpactItem, RolePermissionsImpact in
  lib/api-spec/openapi.yaml.
- Regenerated lib/api-client-react/src/generated/* (new
  usePreviewRolePermissionsImpact hook).

UI
- artifacts/tx-os/src/pages/admin.tsx RolesPanel:
  - Debounced (350ms) on-demand fetch only when at least one permission
    has been unchecked relative to the saved state.
  - Inline amber summary panel listing each removed permission with
    user/group counts, plus a total-affected-users line.
  - Confirmation modal before save when totalAffectedUsers > 0.
- Added EN/AR translations (impactTitle, impactPerPermission,
  impactViaGroups, impactTotal, confirmRemoval*) using i18next
  _one/_other plural keys to match existing pluralized strings.

Tests
- New artifacts/api-server/tests/role-permissions-impact.test.mjs
  (6 tests, all green): empty removals, group-derived impact,
  "covered by another role" exclusion, 404 on unknown role, 400 on
  invalid body, 403 for non-admin.
- Existing roles-crud tests still pass.
- E2E flow verified end to end via the testing tool: amber summary
  appears, confirmation dialog gates the destructive save, and the
  resulting permission set persists.

Code-review follow-ups (applied in this same task)
- Tightened the OpenAPI 404 description for the new endpoint to clarify
  that unknown permission IDs in the body are tolerated (only a missing
  role yields 404). Regenerated the API client.
- Added a request-sequencing guard in the role editor so a stale
  in-flight impact-preview response cannot overwrite a newer selection.
- When the impact preview fails while removals exist, the inline panel
  now shows an explicit error message and the Save button is disabled
  until the user resolves it (e.g. by changing the selection so the
  preview can re-run successfully). EN/AR translations added under
  admin.roles.impactError.

Notes
- The pre-existing test workflow failure (ECONNREFUSED on 127.0.0.1:8080
  before the API server is ready) is unrelated and already tracked.
- replit.md not updated — change is endpoint-level and does not alter
  documented architecture.
- A modification to artifacts/tx-os/public/opengraph.jpg may show up in
  the diff; it is a build artifact regenerated by the vite dev server
  on restart and is not part of this change.
2026-04-29 12:45:19 +00:00
Riyadh 791e0ddaf8 Improve tap target size for attendee elements on touch devices
Adjusted CSS for attendee elements to increase tap target size by adding padding and min-height, improving usability on touch devices like iPads.
2026-04-29 12:44:58 +00:00
Riyadh 48bf09a628 Task #140: 3 attendees-cell refinements in Executive Meetings schedule
(1) Clearing an attendee's name now removes the attendee row instead of
saving an empty record. saveAttendeeName detects empty stripped HTML
(tags + &nbsp; + whitespace) and PUTs the array with the row removed
and sortOrder repacked.

(2) Inline "+ Add attendee" button next to each attendee group, plus
"+ Virtual / + Internal / + External" chips for groups not yet present
in a split cell. Implemented as an optimistic ghost row — clicking +
sets a single global pendingAttendee state and renders a synthetic
EditableCell at the end of the matching group. Only when the user
types a non-empty name and blurs do we PUT the new attendee. Empty
blur or Escape just clears the pending state — no orphan rows reach
the API. The API stays strict (name.min(1)).

(3) Always-visible dashed underline on each attendee EditableCell
makes the multi-attendee wrapped layout discoverable as click targets;
hidden in edit mode (data-[editing=true]) and on print. The group
label ("— Virtual attendance — Webex —") is now pointer-events:none
so clicks pass through to whatever sits underneath instead of being
swallowed by the header text.

Implementation notes:
- EditableCell gained two new props (forceSaveOnBlur, onCancel) so the
  ghost row can commit/discard cleanly even when the editor is empty.
- pendingAttendee is a single global state (only one ghost open at a
  time across the whole page). All other rows hide their + buttons
  while a ghost is open to prevent the user from orphaning typing.
- Locale keys added in ar.json + en.json under
  executiveMeetings.schedule.{addAttendee, addVirtualAttendee, …,
  newAttendeeAria, editAttendeeAria}.
- Backend executive-meetings.ts unchanged — attendeeSchema still
  requires non-empty name.

Verified end-to-end:
- Add attendee, commit on blur, delete by clearing, cancel by Escape
  on empty.
- Cross-row gating: opening a ghost in row A hides "+" buttons in
  row B until the ghost is committed/cancelled.
- Webex/virtual header click no longer enters edit mode.

Pre-existing TypeScript errors in admin.tsx (codegen drift from #96)
and 3 failing api-server tests are unrelated and out of scope.

Follow-ups proposed: #144 (inline edit attendee titles),
#145 (move attendees between virtual/internal/external groups).
2026-04-29 12:42:45 +00:00
Riyadh 1b306ad16b Task #137: Polish Executive Meetings schedule table (5 refinements + post-review fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect reviews:
- Add row scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
  autoEditTitleId hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts.
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (start > end) raises a destructive toast with
  i18n key executiveMeetings.schedule.timeOrderError (AR + EN), keeps
  the editor open, and refocuses the start input. Equal start/end is
  valid per spec. Server errors also toasted.
- Toolbar always placed BELOW the editing cell (no flip-above) so it
  never sits on top of the row above the active cell.
- Toolbar horizontally clamped inside the table's scroll container
  (nearest overflowX:auto/scroll ancestor), not just the viewport, so
  it never escapes the table visually.
- Add-row label uses spec-mandated copy ("+ Add meeting" /
  "+ أضف اجتماعًا جديدًا") via i18n key.
- Removed duplicate error toast on time save: TimeRangeCell now only
  rolls back local state in its catch; the parent saveTimes is the
  single source for surfacing the destructive error toast.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + post-review fixes pass.
  * Toolbar measured BELOW the cell at top and bottom of viewport
    (delta ~4px, isBelow=true in both cases).
  * Toolbar measured INSIDE the table scroll container's left/right.
  * Equal start/end (14:00–14:00) saved without validation toast.
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 12:04:43 +00:00
Riyadh 85f2bb9190 Task #137: Polish Executive Meetings schedule table (5 refinements + post-review fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect reviews:
- Add row scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
  autoEditTitleId hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts.
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (start > end) raises a destructive toast with
  i18n key executiveMeetings.schedule.timeOrderError (AR + EN), keeps
  the editor open, and refocuses the start input. Equal start/end is
  valid per spec. Server errors also toasted.
- Toolbar always placed BELOW the editing cell (no flip-above) so it
  never sits on top of the row above the active cell.
- Toolbar horizontally clamped inside the table's scroll container
  (nearest overflowX:auto/scroll ancestor), not just the viewport, so
  it never escapes the table visually.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + post-review fixes pass.
  * Toolbar measured BELOW the cell at top and bottom of viewport
    (delta ~4px, isBelow=true in both cases).
  * Toolbar measured INSIDE the table scroll container's left/right.
  * Equal start/end (14:00–14:00) saved without validation toast.
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 11:59:00 +00:00
Riyadh 86bd8aa66e Task #137: Polish Executive Meetings schedule table (5 refinements + 4 follow-up fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.

Follow-up fixes from architect review:
- Add row now scrolls the new row into view (requestAnimationFrame +
  scrollIntoView center) and auto-opens the title cell in edit mode
  (createRow captures POST response id, threads autoEditTitleId through
  ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
  em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (end <= start, including equal times) now raises a
  destructive toast with i18n key executiveMeetings.schedule.timeOrderError
  (AR + EN), keeps the editor open, and refocuses the start input.
  Server errors also toasted.
- autoEditTitleId state hardened with a 1.5s fallback clear so it can't go
  stale if the title cell never mounts (e.g., column hidden).
- Toolbar prefers placement BELOW the editing cell (only flips above
  when no room below in viewport), so it never covers the row above.

Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json

Verification:
- E2E (Playwright): all 5 features + 4 fixes pass; toolbar prefer-below
  measured cell.bottom=276.84 / toolbar.top=280.84 on a top-of-viewport
  row (correctly flips above only at viewport bottom).
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
  3 api-server tests) are unrelated and untouched.
2026-04-29 11:50:41 +00:00
Riyadh 3dbd43c8cd Task #98: Warn admins before renaming a role used in code references
- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
  guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
  renamedFrom, and renamedTo when the role name changes (in addition to
  existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
  regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
  updated intentionally to preserve `export * from './manual'` in
  api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
  on open and shows an amber warning banner with from/to text plus a
  usage line (users + groups counts) whenever the trimmed name input
  differs from the original. Warning is suppressed for system or
  protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
  renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
  session — it is unrelated to this task.

Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.

Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).
2026-04-29 10:57:44 +00:00
Riyadh 4cfa236fa3 Git commit prior to merge 2026-04-29 10:57:44 +00:00
Riyadh 342b59de4b Git commit prior to merge 2026-04-29 10:32:54 +00:00