a72c00fe19f5e4a6a98ed7ab690077bbee72142e
149 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a72c00fe19 |
Task #273: 5-minute pre-meeting alert for Executive Meetings
- New `executive_meeting_alert_state` table (per-user, per-meeting) tracking `dismissed`/`acknowledged`. Migration via `pnpm --filter @workspace/db run push-force`. - New API routes in artifacts/api-server/src/routes/executive-meetings.ts: GET /alert-state, POST /:id/alert-state, POST /:id/postpone-minutes, POST /:id/reschedule, POST /:id/cancel. All three mutation routes acquire a row lock with SELECT ... FOR UPDATE inside the transaction, compute oldValue from the locked snapshot, run conflict detection in the same tx, and write the audit row before commit. Cancel is idempotent. Postpone-minutes rejects ranges that would cross midnight (use reschedule for cross-day moves). - Alert-state route uses race-safe onConflictDoNothing upsert + a conditional UPDATE ... RETURNING so transition audits never duplicate. - New i18n keys `executiveMeetings.alert.*` in en.json + ar.json, including the cancel-confirm prompt. - New component artifacts/tx-os/src/components/executive-meetings/ upcoming-meeting-alert.tsx — globally mounted in App.tsx inside AuthProvider. Draggable with localStorage position persistence, RTL-aware, polls every 30s, shows start–end window, postpone-by- minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a Cancel-meeting flow that requires an explicit confirm step before the destructive call fires (gated on confirmCancel state). - Playwright spec executive-meetings-upcoming-alert.spec.mjs with 6 scenarios: appear+Done, postpone-10 shifts times, cancel-with- confirm, dismiss audit, postpone-chip+conflict-warning toast, AR/RTL render. All 6 pass. - replit.md updated with the new table, routes, and migration step. Pre-existing tsc errors at lines 489, 605, and 2039 of executive-meetings.ts are not touched by this task. |
||
|
|
a29f7a25c6 |
Add browser test for drag-to-reorder schedule columns (task #187)
Adds a Playwright scenario to artifacts/tx-os/tests/executive-meetings-schedule-features.spec.mjs that: - Logs in as admin, seeds one meeting on a far-future date, and lands on the schedule for that date. - Hops to the Settings tab to confirm the column customizer panel (em-customize-columns-panel) still surfaces the feature, then bounces back to the schedule. - Enters edit mode (required for SortableHeader to register dnd-kit attributes/listeners) and asserts the default header order (number, meeting, attendees, time) via th[data-testid^=em-col-header-]. - Drags the "attendees" header above the "meeting" header using a warm-up move past the 6px PointerSensor activation distance and a stepped pointer move whose end point lands slightly LEFT of the target's horizontal center (horizontalListSortingStrategy needs the drop point on the leading side to insert before the target). - Polls the rendered headers for the new order (number, attendees, meeting, time), then reads em-schedule-cols-v1 from localStorage to confirm persistence. - Reloads the page and re-asserts the new order to prove it restores from localStorage rather than reverting to DEFAULT_COLUMNS. Deviation from the task wording: the task description says the test should "open the customize-columns popover" and "drag one column chip above another". The customize panel is no longer a popover — it was moved into the Settings tab in #265 — and it never had draggable chips; column reordering is wired to the SortableHeader cells inside the schedule's floating thead. The new test honors the spirit of the task by visiting the Settings panel for sanity, then performing the actual reorder gesture on the table headers (the only mechanism the codebase exposes). Validation: the new test passes in isolation and as part of the schedule-features suite. One unrelated existing test ("custom highlight color paints the current meeting's box-shadow ring") is currently flaky/failing on its own without my changes; left untouched as it is outside this task's scope. Replit-Task-Id: 06c14e68-096b-407d-86b9-bd5a45674aee |
||
|
|
d55c7ee8bb |
Add browser test for editing attendee names with formatting (Task #186)
Original task: cover the inline attendee-name edit flow in the Executive Meetings schedule. The attendee row uses the same EditableCell + Tiptap toolbar as titles (parent task #122), but until now only the title path had end-to-end coverage. A regression in attendee-name formatting would only have surfaced via manual QA. Changes: - Extended artifacts/tx-os/tests/executive-meetings-schedule-features.spec.mjs with a new scenario: * seeds a meeting + one plain-text attendee directly in the DB, * logs in as admin, navigates to the schedule, jumps to the seeded date, flips on edit mode, * clicks the inline attendee EditableCell, selects all, applies bold + the red color swatch via the toolbar, and saves via the check button while waiting for PUT /api/executive-meetings/:id/attendees to succeed, * reloads the page, re-navigates to the date, and asserts the rendered HTML in the cell still contains <strong>/<b> and the red color (#dc2626 or rgb(220,38,38)), * additionally queries executive_meeting_attendees.name in the DB to confirm the formatted HTML round-tripped through the server, not just survived in the local Tiptap document. - Added a small insertAttendee helper in the same spec to seed initial state without driving the manage-dialog flow. - The existing afterAll already deletes attendee rows for the created meetings (cascade-safe), so no cleanup changes were needed. Verification: ran the new test in isolation (passed in 9.1s) and the full schedule-features spec (5/5 passed in 40.1s). No code changes outside the test file. Follow-up filed: #270 — browser test for clearing an attendee's name to trigger the delete-row gesture (separate documented behavior path with no end-to-end coverage today). Replit-Task-Id: 68a0ce72-6cdf-45e0-a7c7-4fea9c833fe7 |
||
|
|
699cfafcb4 |
Migrate admin user editor + Review pop-up to in-house Dialog (a11y)
Original task: Make admin pop-ups close with Escape and trap keyboard focus (#185). Changes - artifacts/tx-os/src/pages/admin.tsx - UserGroupsEditor and its inner "Review changes" pop-up no longer use the hand-rolled fixed-inset overlay. Both now use the in-house Radix-based Dialog (Dialog/DialogContent/DialogHeader/DialogTitle/ DialogDescription/DialogFooter) so Escape closes only the topmost dialog, Tab/Shift+Tab is trapped, and the close button + aria-modal semantics come for free. - Removed the manual `keydown` Escape useEffect that was previously needed for the Review pop-up. - Preserved all existing data-testids (edit-user-save, edit-user-review-dialog, edit-user-review-back, etc.) plus added a new edit-user-dialog testid for the editor shell. - Kept the original glass-panel look by passing border-0 / shadow-none and rounded-3xl through DialogContent's className. - Focus return: Radix's automatic restoration is unreliable for nested dialogs. Added two explicit hops: - The Review dialog uses onCloseAutoFocus to refocus the Save button via a saveBtnRef. - The editor dialog captures whatever was focused when it first rendered (the pencil icon) into triggerElementRef, and onCloseAutoFocus refocuses it. Verified the pencil button is what the test sees. Tests - artifacts/tx-os/tests/admin-user-edit-review.spec.mjs - New test "Escape closes topmost dialog; Tab keeps focus inside; focus returns to opener" runs in both en and ar: - Esc closes only the Review (editor stays open) and focus returns to the Save button. - Tab x15 and Shift+Tab x5 keep document.activeElement inside the editor dialog. - Esc on the editor closes it and returns focus to the pencil. - All 4 specs in this file pass (~58s). No deviations from the task; scope intentionally limited to the user editor + Review pop-up. The same pattern in the Groups/Roles/Apps editors was filed as follow-up #266. Replit-Task-Id: fc613d10-06d0-460f-b1a4-0a65ff021d4a |
||
|
|
ad84a8abc6 |
#267: Freeze top bar, schedule heading, and table column header on scroll
Top bar and schedule-heading row already used `position: sticky` with dynamic offsets published as `--em-header-h` / `--em-heading-h` via ResizeObserver. The table column header is the new piece. Approach: a floating sticky overlay (`em-sticky-thead`) rendered as a DOM-sibling above the horizontally-scrollable wrapper, scroll-synced in JS and column-width-measured from the first valid tbody row via ResizeObserver. The original `<thead>` is hidden in screen mode but kept (no testids, no Sortable) with `print:table-header-group` so column labels still appear at the top of every printed page. This replaces the rejected first attempt that relied on `position:sticky` inside `overflow-x-auto`, which only worked at xl+ viewports. The floating overlay sticks reliably at mobile (414px), tablet (~900px), desktop, and in RTL — all four covered by an expanded sticky-header e2e spec. Drag-reorder, resize handles, and the bulk-select tri-state checkbox now live exclusively in the floating thead, eliminating the duplicate- testid concern from having two interactive headers in the DOM. Tests: 4/4 sticky-header, 5/5 bulk-actions, 6/6 edit-toggle, 7/7 keyboard editing, 1/1 touch reorder. The one failing schedule-features case (custom highlight color) reproduces on the pre-change baseline and is pre-existing flake unrelated to this work. Files: - artifacts/tx-os/src/pages/executive-meetings.tsx - artifacts/tx-os/tests/executive-meetings-sticky-header.spec.mjs |
||
|
|
69b5deef19 |
#267 freeze top bar, schedule heading, and table column header on scroll
Executive Meetings page: keep the page header, the schedule title row (heading + edit toggle + date picker), and the table column header visible while the user scrolls a long meetings list. Implementation: - ExecutiveMeetingsPage publishes the live header height as a CSS variable `--em-header-h` on the page root (data-em-root) via a ResizeObserver. The <header> is now `sticky top-0 z-40` with print:hidden so the print layout is untouched. - ScheduleSection wraps the title row in a `sticky top:var(--em-header-h) z-30` div with a soft full-bleed background and bottom shadow, and publishes its own height as `--em-heading-h` on the page root via a second ResizeObserver. Cleanup resets the var to "0px" so other sections without a heading don't inherit a stale offset. - SortableHeader's <th> is now `position:sticky` with `top: calc(var(--em-header-h) + var(--em-heading-h)); zIndex:5` and a solid `bg-[#0B1E3F]` so scrolled rows don't bleed through. - The table wrapper changes from `overflow-x-auto` to `overflow-x-auto xl:overflow-x-visible` so at >=xl the inner thead's nearest scrolling ancestor is the viewport (sticky works). Below xl the wrapper retains horizontal scroll and the column header gracefully degrades to non-sticky; the page header + heading row still stick at every width. - Print mode is preserved via `print:hidden` (sticky bars) and `print:!static` / `print:!shadow-none` overrides (sticky headers). Tests: - New e2e spec executive-meetings-sticky-header.spec.mjs covers three scenarios: desktop LTR (all three layers stick flush), Arabic RTL (header + heading stick), and narrow viewport (header + heading stick, column header documented to degrade). - Re-ran related schedule e2e specs (bulk-actions 5/5, edit-toggle 6/6, schedule-features 4/4) and the full API suite (226/226 sequential) — all pass. New testids: `em-page-header`, `em-schedule-heading-bar`. Existing `em-schedule-heading` testid retained on the inner h2. No drift from task plan. |
||
|
|
aecc5f5eed |
Restore opengraph image to its previous state
Revert changes to artifacts/tx-os/public/opengraph.jpg to resolve unintended modifications. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 8b4b2f5f-b778-466d-967b-d07190e7cccb Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/zG2WcPi Replit-Helium-Checkpoint-Created: true |
||
|
|
310baa41ab |
#265: unify Executive Meetings header into single Settings tab
Header
- Removed the standalone Font Settings nav button and the bilingual
language toggle. The header now exposes only Export PDF.
- Renamed the SECTIONS key `fontSettings` → `settings` (Settings icon
retained) and updated visibility checks + render switch.
Settings tab
- New SettingsSection wraps the existing FontSettingsSection plus a
new ColumnsCustomizerPanel (extracted from the old popover body) so
column visibility / current-meeting highlight live with the rest of
user prefs.
- Removed the old ColumnsCustomizer popover trigger from the schedule
toolbar; the inline panel inside Settings is the single entry point.
State lifting
- columns/setColumns and highlightPrefs/setHighlightPrefs lifted from
ScheduleSection up to ExecutiveMeetingsPage so both tabs read/write
the same state. Storage keys (COLS_STORAGE_KEY, HIGHLIGHT_STORAGE_KEY)
unchanged so existing user prefs continue to load.
- Per code-reviewer follow-up: moved the columns localStorage write
effect up to the page as well so edits made in Settings persist even
when ScheduleSection is unmounted.
Locales
- ar.json + en.json: renamed nav.fontSettings → nav.settings
("الإعدادات" / "Settings"); removed the now-unused
executiveMeetings.fontSettings header label.
- Removed unused Languages and Type lucide imports.
Tests
- Updated executive-meetings-bulk-actions.spec.mjs and
executive-meetings-schedule-features.spec.mjs to navigate via
em-nav-settings instead of the removed em-customize-columns-trigger.
- All 226 sequential api tests pass; both updated playwright specs
pass.
|
||
|
|
53d13a5939 |
Add browser tests for the inline dependency counts on the admin lists
Task #184. The admin Apps / Services / Users panels render small testid'd chips under each row that summarize how many dependent records reference the entity. Until now, that behavior had no regression coverage, which made it easy to silently drop the chips in a future refactor. Adds artifacts/tx-os/tests/admin-inline-dependency-counts.spec.mjs. The spec seeds, per locale (en + ar): - One app with every dep kind populated (groups + restrictions + opens) and one app with none. - One service with orders and one service with none. - One user with notes / orders / conversations / messages and one bare user. Asserts that: - The container `[data-testid="<entity>-counts-<id>"]` and each populated child chip render with the localized count text. - The container is omitted entirely (count = 0 in DOM) when the entity has zero dependents — anchored by the row's delete button so we know the row itself rendered. - <html dir> matches the locale (rtl in Arabic, ltr in English) and the chip row's computed flex direction follows it, so a future hard-coded LTR regression would fail. Two implementation notes worth flagging: - Switching admin sections via `page.goto` only changes the URL hash on the same path, which does NOT trigger a reload, and the admin page only reads `?section=` once on mount. The spec pairs each goto with `page.reload()` so the section actually re-applies. - The userWithDeps order rides on serviceWithOrders (instead of serviceNoOrders) so the bare service truly has zero orders. As a side effect serviceWithOrders has 2 orders, so the chip text assertion only checks the localized noun and a non-zero digit rather than pinning the exact count. Cleanup runs in afterAll: deletes messages / conversations / notes / service_orders before users (sender_id and created_by have no ON DELETE rule) and service_orders before services (RESTRICT on service_id), matching the constraints in lib/db/src/schema. Verified locally: both new tests pass and the existing admin specs (admin-create-group-app-visibility, admin-user-edit-review en/ar) still pass. Cleanup vs the previous attempt: removed an ad-hoc tests/_debug.mjs that was used to reproduce the hash-only-navigation bug and reverted artifacts/tx-os/public/opengraph.jpg, which had been touched by an unrelated incidental rebuild during local testing. Replit-Task-Id: 8a1f407d-2a22-4e70-b8ad-25ff7d9b0dae |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
c0e55752a3 |
Task #183: clickable dependency counts on admin Apps/Services/Users
Turned the inline dependency-count badges on each admin row into
focusable, keyboard-accessible buttons that open a drill-in modal
listing the actual rows behind the count.
Backend (artifacts/api-server/src/routes):
- apps.ts: GET /admin/apps/:id/dependents/{groups,restrictions,opens}
- services.ts: GET /admin/services/:id/dependents/orders
- users.ts: GET /admin/users/:id/dependents/{notes,orders,conversations,messages}
All paginated (limit default 50, max 200; offset) returning
{items, totalCount, limit, offset, nextOffset}. Restrictions joins
the role names that include each required permission.
OpenAPI:
- Added 8 path operations + 16 schemas (Item + Page) and re-ran
`pnpm --filter @workspace/api-spec run codegen`.
Frontend (artifacts/tx-os/src/pages/admin.tsx):
- New DependencyDrillIn component reuses DrillInShell (Escape +
backdrop close, RTL/LTR safe) and the existing LoadMoreSection
pagination pattern from AppOpensDrillIn.
- Each count part in Apps, Services, and Users panels is now a
<button> with a unique data-testid (e.g. app-counts-groups-213,
user-counts-messages-5) and an aria-label that reads
"View {count}".
- AdminPage owns dependencyTarget state for Apps/Services counts;
UsersPanel owns its own (it already encapsulates user list).
Translations:
- Added admin.dependents.* (titles, subtitles, empty/error/load
labels, conversation/order/message helper strings, order status
enum) to en.json and ar.json.
Verification: - tx-os typecheck clean; api-server has only the pre-existing
executive-meetings.ts errors (untouched).
- e2e tested via runTest: login as admin, opened Apps panel,
drilled into groups + opens (Load more grew 50→100), drilled into
Tea orders, drilled into user 5 conversations and messages,
switched to Arabic/RTL and re-opened the Groups drill-in to
confirm Arabic rendering with no raw i18n keys.
Replit-Task-Id: fe96a05b-325f-4e1f-901b-3a2235fb24b5
|
||
|
|
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). |
||
|
|
90c319fb25 |
Show inline dependency counts on the Roles admin list (Task #182)
The Apps, Services, Users, and Groups admin panels already surface their dependency counts inline so admins know what's affected before clicking. The Roles panel previously hid this — admins had to open the delete dialog to see how many users/groups would be affected. This change adds the same inline display to the Roles panel for consistency. Changes - lib/api-spec/openapi.yaml: Added optional `userCount` and `groupCount` fields to the `Role` schema (matching the App pattern: optional, populated only by the admin list endpoint, with descriptive comments). - artifacts/api-server/src/routes/roles.ts: GET /roles now batches two grouped count queries (user_roles, group_roles) and merges the counts into each list item — same shape as GET /apps. Empty-list short-circuits before running the aggregations. - lib/api-zod/src/generated/api.ts: Regenerated via the api-spec codegen script (orval). ListRolesResponseItem now includes the optional counts. - artifacts/tx-os/src/pages/admin.tsx (RolesPanel): Each role card renders an inline counts row using the existing `admin.roles.usersCount` / `admin.roles.groupsCount` translation keys (no new copy needed). Mirrors the Apps panel pattern: 11px muted-foreground text with bullet separators, only renders when at least one count is > 0, and exposes a `data-testid="role-counts-<id>"` for tests. Notes / deviations - The task description said "the role list endpoint already returns userCount/groupCount" but it didn't — the counts only existed on /roles/:id/usage. Added them to the list endpoint following the same pattern Apps and Groups already use. - The pre-existing admin.roles.usersCount/groupsCount keys have no `_one`/`_other` plural variants; I kept it that way to stay consistent with the Apps panel keys (which also have no plural variants). Verification - `pnpm -w run typecheck` passes for tx-os and roles.ts (pre-existing unrelated typecheck errors in executive-meetings.ts remain — not touched by this change). - e2e test (testing skill, status: success): logged in as the seeded admin, opened the Roles panel, verified inline counts render on the admin and user roles, bullet separator is present, and roles with zero dependencies don't render an empty counts area. Replit-Task-Id: 8c99d912-8b3a-4e80-aca7-ec167e6e75e6 |
||
|
|
eefcf20403 |
Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven narrow-then-defer pattern from #242: - #195 — Plain DELETE /api/services/:id now writes a `service.delete` audit row carrying nameEn + nameAr (force-with-deps still uses the dedicated `service.force_delete`). Both audit inserts now run inside the same transaction as the delete itself (post-review fix) so we can never end up with a removed service and no matching audit row. Added matching `service.delete` formatter case + EN/AR i18n keys, and surfaced nameAr on the existing `service.force_delete` summary. - #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export. openapi.yaml updated, codegen regenerated, server filter wired through parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI got an actor dropdown wired into params + export URL + reset, and a new audit-logs-actor-filter API test (4 cases) covers list narrowing, exclusion, invalid input, and CSV export. - #178 — Formatter unit tests for user.delete (id-only, EN/AR display name resolution, force flag, force + name) and the new service.delete (id-only, EN/AR), 11 new cases (33/33 pass). Skipped #194 — already implemented; users.ts DELETE persists displayName fields and audit-summary already renders user.deleteWithName/forceDeleteWithName. Deferred via follow-ups (no duplicate of existing #182/#183/#184): - F1: #196 recent-activity endpoint + 5 admin panels - F2: #205+#206+#208 permission history CSV/name resolution/timeline - F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs Also reverts an unrelated stray binary change to artifacts/tx-os/public/opengraph.jpg that got rolled into the prior auto-commit — restored to its previous content. Validation: tx-os typecheck clean; pre-existing executive-meetings.ts errors not regressed; all targeted server tests pass (delete-force-warnings 10, audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new actor-filter 4, broader audit/services sweep 40); e2e test verified actor dropdown rendering, filter behavior, readable Arabic service.delete summary, and CSV export honoring the filter. |
||
|
|
b3ad701ba6 |
Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven narrow-then-defer pattern from #242: - #195 — Plain DELETE /api/services/:id now writes a `service.delete` audit row carrying nameEn + nameAr (force-with-deps still uses the dedicated `service.force_delete`). Added matching `service.delete` formatter case + EN/AR i18n keys, and surfaced nameAr on the existing `service.force_delete` summary. - #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export. openapi.yaml updated, codegen regenerated, server filter wired through parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI got an actor dropdown wired into params + export URL + reset, and a new audit-logs-actor-filter API test (4 cases) covers list narrowing, exclusion, invalid input, and CSV export. - #178 — Formatter unit tests for user.delete (id-only, EN/AR display name resolution, force flag, force + name) and the new service.delete (id-only, EN/AR), 11 new cases (33/33 pass). Skipped #194 — already implemented; users.ts DELETE persists displayName fields and audit-summary already renders user.deleteWithName/forceDeleteWithName. Deferred via follow-ups (no duplicate of existing #182/#183/#184): - F1: #196 recent-activity endpoint + 5 admin panels - F2: #205+#206+#208 permission history CSV/name resolution/timeline - F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs Validation: tx-os typecheck clean; pre-existing executive-meetings.ts errors not regressed; all targeted server tests pass (delete-force-warnings 10, audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new actor-filter 4, broader audit/services sweep 40); e2e test verified actor dropdown rendering, filter behavior, readable Arabic service.delete summary, and CSV export honoring the filter. |
||
|
|
a220794036 |
Task #242: Executive Meetings UX — print page removal, inter-person chip, bulk delete undo
Original umbrella covered #144, #145, #168, #169, #199, #200, #211, #217, #222. Three landed here; #200 was already implemented; remaining five proposed as follow-ups #248–#250. #169 — Removed dead /executive-meetings/print route: - Deleted artifacts/tx-os/src/pages/executive-meetings-print.tsx - Removed import + Route from App.tsx - Removed executiveMeetings.print blocks from en.json and ar.json #222 — "+ شخص هنا" chip in inter-person gaps: - Added addPersonHere i18n key (ar/en) - Threaded addPersonHereLabel through AttendeeFlowSharedProps → flowProps → AttendeeFlow - Renders chip after a person row when next item is also a person (not a subheading); reuses existing onStartAdd("person", i+1) plumbing - Test IDs em-add-person-after-row-${i} / em-add-person-after-${i} #199 — Bulk delete undo via toast action: - ToastAction wired into deleteSelectedMeetings result toast - Snapshots captured client-side before DELETE - Undo recreates each row via existing POST /api/executive-meetings (omitting dailyNumber to avoid 409s on stolen slots), then PATCH if merge fields existed - Single-fire guard prevents double-click duplicates - Updated bulkDeleteConfirm to drop the now-untrue "cannot be undone" line - New strings: bulkDeleteUndo / bulkDeleteUndone / bulkDeleteUndoPartial / bulkDeleteUndoFailed in ar+en Verification: - tsc --noEmit clean for all touched files (admin.tsx errors are pre-existing, unrelated to this diff) - Playwright executive-meetings-bulk-actions.spec.mjs: 5/5 pass - Pre-existing flake meeting_created socket fan-out test passes in isolation (unrelated to changes) |
||
|
|
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 |
||
|
|
ca727c3759 |
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 (`<script>…`) 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 (`<script>`, `<script>`, 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). Also added (e2e) - New Playwright spec `artifacts/tx-os/tests/executive-meetings-subheading-chip-hidden.spec.mjs` covers item #235: seeds a multi-group meeting (virtual + internal), opens the cell-level "+ subheading" chip, asserts the chip unmounts while the pending input is open, then re-mounts after Escape cancels the input. Passes locally (~7s). Drift from the original umbrella - The umbrella listed 9 Playwright e2e specs (#170, #186, #187, #188, #201, #212, #214, #218, #235). #235 landed in this diff; the other 8 remain deferred. Each remaining spec is a 100–300 line standalone file (no shared helpers in this repo) and bundling all 8 would more than double the existing e2e count for one task. Each remains as its own PENDING project task and can be picked up incrementally. 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. |
||
|
|
4c1e417e27 |
Add automated tests for audit log readable summaries
Original task: add unit tests for the new `formatAuditSummary` formatter
and an API-level test asserting the enriched group sub-resource audit
metadata, and wire both into the existing `test` workflow.
What changed:
- Extracted `formatAuditSummary` and its helpers (`asRecord`, `asString`,
`asNumber`, `unitLabel`, `appName`, `linkedAppName`, `plainName`,
`changeCount`) out of `artifacts/tx-os/src/pages/admin.tsx` into a new
`artifacts/tx-os/src/lib/audit-summary.ts` module so the pure formatter
can be unit-tested without the React tree. `admin.tsx` now imports the
helpers from that module.
- Added `artifacts/tx-os/src/__tests__/audit-summary.test.mjs` with 22
Node test-runner cases covering app rename (EN + AR), app-update
fallback, group rename, group multi-field update, registration toggle
(open / close / with-other-changes), and every group.user/app/role
add/remove name vs id-only branch, plus the unknown-action default.
- Added `pnpm --filter @workspace/tx-os test` (Node 24's native
TypeScript loader runs the .mjs tests against the .ts module directly).
- Added `artifacts/api-server/tests/group-audit-metadata.test.mjs` using
the same harness as `groups-crud.test.mjs`. It hits POST/DELETE
`/api/groups/:id/{users,apps,roles}/:targetId` and reads the resulting
`audit_logs.metadata`, asserting `username`, `appSlug` /`appNameEn` /
`appNameAr`, and `roleName` are persisted alongside the raw IDs.
- Updated the `test` workflow to run the tx-os unit tests before the
api-server tests, then the tx-os e2e tests.
Verification: all 22 tx-os unit tests pass via the new pnpm script, and
all 6 new api-server audit-metadata tests pass against a live server.
The overall api-server suite still has pre-existing flakes
(executive-meetings notifications/status transitions, and the
count-based group invariant in groups-crud.test.mjs) that are unrelated
to this change; both flake clusters are filed as follow-up tasks.
Replit-Task-Id: 182cd4ed-c55c-43e3-b10b-8147a9611fd4
|
||
|
|
eb7d15ca7e |
Show readable names in audit log for top-level deletions
Task: #177 — Make user/app/role deletion audit rows render readable names ("Deleted user @alice (Alice Smith)", "Deleted app 'Notes'", "Deleted role 'Editor'") instead of relying on whatever the route happened to capture. Backend metadata changes: - artifacts/api-server/src/routes/users.ts (user.delete): now also persists displayNameEn and displayNameAr alongside the existing username/email. - artifacts/api-server/src/routes/apps.ts (app.delete): renamed the metadata keys slug/nameAr/nameEn → appSlug/appNameAr/appNameEn so app sub-resource events and top-level deletes share one prefix. - artifacts/api-server/src/routes/roles.ts (role.delete): renamed the metadata key name → roleName, matching group.role.add/remove. Frontend formatter (artifacts/tx-os/src/pages/admin.tsx): - appName helper now reads both legacy (slug/nameEn/nameAr) and new (appSlug/appNameEn/appNameAr) keys so old rows still render. - role.delete case prefers roleName, falls back to legacy name. - user.delete case picks the user's localized display name and uses new locale strings user.deleteWithName / user.forceDeleteWithName when present; falls back to the existing username-only strings. - forceDeletedEntityName also accepts appNameEn/appNameAr/appSlug so force-deleted apps still get their inline name chip. Locales: - artifacts/tx-os/src/locales/{en,ar}.json: added admin.audit.summary.user.deleteWithName and forceDeleteWithName. Test updates: - artifacts/api-server/tests/audit-log-coverage.test.mjs: updated the role.delete and app.delete (no-deps) assertions to read the new metadata key names. The user.delete assertions kept working as-is since username/email/force are unchanged. No DB migration was required — audit_logs.metadata is already JSON. Legacy rows continue to render via the formatter fallbacks called out in the task description. Replit-Task-Id: a25d35dd-5005-4e57-96a5-580016f35e46 |
||
|
|
56524cf0de |
Friendly summaries for app.permission.add/remove audit entries
Task #174: The new `app.permission.add` and `app.permission.remove` audit log actions emitted by `artifacts/api-server/src/routes/apps.ts` were falling through `formatAuditSummary`'s default branch, so the admin audit log only displayed the raw action string instead of a human-readable sentence like the existing `role.permission.*` entries. Changes: - artifacts/tx-os/src/pages/admin.tsx: added `app.permission.add` and `app.permission.remove` cases to `formatAuditSummary`. They reuse the existing `appName(meta, lang, targetId)` helper (which already handles the nameEn/nameAr/slug fallback) and mirror the existing role.permission.* permission-name fallback (permissionName -> #permissionId -> "?"). - artifacts/tx-os/src/locales/en.json: added `admin.audit.summary.app.permissionAdd` / `admin.audit.summary.app.permissionRemove`, matching the wording of the role.* variants ("Added permission '{{permission}}' to app '{{name}}'", "Removed permission '{{permission}}' from app '{{name}}'"). - artifacts/tx-os/src/locales/ar.json: added the Arabic equivalents ("تمت إضافة الصلاحية '...' إلى التطبيق '...'", "تمت إزالة الصلاحية '...' من التطبيق '...'"). Verification: - e2e tested via runTest: created a new app, added/removed a permission via the API to emit both audit entries, opened the admin audit log, and confirmed both rows render the friendly English summary; switched the UI to Arabic and confirmed the same rows render the Arabic summary (no raw "app.permission.add" / "app.permission.remove" strings shown). - JSON files validated; no new TypeScript errors introduced near the edited lines (the pre-existing 36 codegen-related errors in admin.tsx are unrelated). No deviations from the task scope. Replit-Task-Id: eb2f3eea-089a-4afc-803c-2580cbf1cfc9 |
||
|
|
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
|
||
|
|
96993df361 |
Task #234: Show one cell-level "+ عنوان فرعي" chip instead of one per group
Original task: in split-mode meeting cells (cells where 2+ attendance
groups are visible), the trailing "+ عنوان فرعي" chip was being
rendered once per AttendeeGroup. The user reported seeing two chips
(one for the internal group, one for the external group) and asked
"ليش اثنين؟". Per the user's choice in the clarifying interview, we
keep ONE chip at the bottom of the cell, and clicking it adds a new
subheading to the LAST visible attendance group in render order
(virtual → internal → external).
Implementation:
- Added optional `suppressTrailingSubheadingChip` prop to
`AttendeeFlowSharedProps` (defaults to false, so single-group cells
are unchanged).
- `AttendeeFlow` skips rendering its trailing per-group "+ عنوان فرعي"
<li> when the prop is true. The per-group "+" person <li> and all
after-section chips are unaffected.
- In the `hasSplit` branch of `AttendeesCell`, every per-group
`AttendeeGroup` is now passed `suppressTrailingSubheadingChip`, and
one new cell-level chip is rendered after the missing-groups chip
block with `data-testid="em-add-subheading-cell-${meeting.id}"`. The
chip is gated by the same `canMutate && !hasAnyPending && startAdd`
guard as the other add chips. Its onClick computes
`lastVisibleAddType` at click-time as `external > internal > virtual`
and calls `startAdd(lastVisibleAddType, "subheading")`.
Tests:
- Updated `Schedule [en|ar]: top "+ subheading" chip is NEVER
rendered…` so its mixed-meeting cell now asserts the per-group
`em-add-subheading-{addType}` testids are absent in split mode and
the new cell-level testid is visible. Per-group "+" person testids
still prove all three groups mounted.
- Added `Schedule [en|ar]: split-mode cell shows ONE cell-level "+
subheading" chip and routes to the LAST visible group`. Seeds
virtual + internal + external attendees, asserts ONE cell-level
chip + zero per-group subheading chips, clicks it, types a
subheading, blurs, and verifies the new subheading persists at the
end of the external group with correct kind / attendance_type /
sort_order.
- Added `Schedule [en|ar]: split-mode WITHOUT external — cell-level "+
subheading" chip routes to INTERNAL (last visible)` to lock the
fallback path of the lastVisibleAddType resolution rule
(external > internal > virtual). Seeds virtual + internal only and
verifies the new subheading persists at the end of the internal
group.
Verification:
- tsc clean for executive-meetings.tsx (admin.tsx errors are
pre-existing and unrelated).
- All 4 new+modified tests pass (en+ar both for the modified top-chip
test and the new cell-level chip test).
- The 4 unrelated test failures observed in the full run are
pre-existing flakes (locale state pollution + dnd-kit RTL drag) and
not caused by this change.
- Architect review: PASS. No critical findings.
Follow-up:
- Task #235 (PROPOSED) covers the remaining test gap: locking that
the cell-level chip stays hidden in row B while another row A has
a pending ghost input open (the in-code guard uses
`canMutate && !hasAnyPending && startAdd`).
|
||
|
|
27eadae655 |
Task #234: Show one cell-level "+ عنوان فرعي" chip instead of one per group
Original task: in split-mode meeting cells (cells where 2+ attendance
groups are visible), the trailing "+ عنوان فرعي" chip was being
rendered once per AttendeeGroup. The user reported seeing two chips
(one for the internal group, one for the external group) and asked
"ليش اثنين؟". Per the user's choice in the clarifying interview, we
keep ONE chip at the bottom of the cell, and clicking it adds a new
subheading to the LAST visible attendance group in render order
(virtual → internal → external).
Implementation:
- Added optional `suppressTrailingSubheadingChip` prop to
`AttendeeFlowSharedProps` (defaults to false, so single-group cells
are unchanged).
- `AttendeeFlow` skips rendering its trailing per-group "+ عنوان فرعي"
<li> when the prop is true. The per-group "+" person <li> and all
after-section chips are unaffected.
- In the `hasSplit` branch of `AttendeesCell`, every per-group
`AttendeeGroup` is now passed `suppressTrailingSubheadingChip`, and
one new cell-level chip is rendered after the missing-groups chip
block with `data-testid="em-add-subheading-cell-${meeting.id}"`. The
chip is gated by the same `canMutate && !hasAnyPending && startAdd`
guard as the other add chips. Its onClick computes
`lastVisibleAddType` at click-time as `external > internal > virtual`
and calls `startAdd(lastVisibleAddType, "subheading")`.
Tests:
- Updated `Schedule [en|ar]: top "+ subheading" chip is NEVER
rendered…` so its mixed-meeting cell now asserts the per-group
`em-add-subheading-{addType}` testids are absent in split mode and
the new cell-level testid is visible. Per-group "+" person testids
still prove all three groups mounted.
- Added `Schedule [en|ar]: split-mode cell shows ONE cell-level "+
subheading" chip and routes to the LAST visible group`. Seeds
virtual + internal + external attendees, asserts ONE cell-level
chip + zero per-group subheading chips, clicks it, types a
subheading, blurs, and verifies the new subheading persists at the
end of the external group with correct kind / attendance_type /
sort_order.
Verification:
- tsc clean for executive-meetings.tsx (admin.tsx errors are
pre-existing and unrelated).
- All 4 new+modified tests pass (en+ar both for the modified top-chip
test and the new cell-level chip test).
- The 4 unrelated test failures observed in the full run are
pre-existing flakes (locale state pollution + dnd-kit RTL drag) and
not caused by this change.
- Architect review: PASS. No critical findings.
Follow-up:
- Task #235 (PROPOSED) covers two test gaps: virtual+internal-only
fallback locking lastVisibleAddType=internal, and chip-hidden gating
while a pending ghost is open in another row.
|
||
|
|
60a18e1c7c |
Task #162: Let admins pre-set required permissions while creating an app
The "Required permissions" section was previously edit-only because `POST /api/apps/:id/permissions` needs an app id, leaving a brief window where a freshly created app was visible to everyone before the admin could re-open the dialog and gate it. The Add app dialog now lets the admin pick required permissions up front and the new app + its `app_permissions` rows are written in a single transaction. Changes: - `lib/api-spec/openapi.yaml`: extended `CreateAppBody` with an optional `permissionIds: integer[]` field. Ran `pnpm --filter @workspace/api-spec run codegen` so `lib/api-zod` and `lib/api-client-react` reflect it. - `artifacts/api-server/src/routes/apps.ts`: `POST /apps` now de-dupes and pre-validates `permissionIds`, returns 404 if any id is unknown (without creating the app), and inside one transaction inserts the app, the `app_permissions` rows (with `.onConflictDoNothing()` against the composite primary key), and a single `permission_audit` row (`previousIds: []`, `newIds: requestedIds`). After the transaction it also writes one `app.permission.add` audit_logs entry per inserted permission so the admin log mirrors the post-create flow. - `artifacts/tx-os/src/pages/admin.tsx`: added `permissionIds: number[]` to `AppForm`, a new `NewAppPermissionsPicker` component (rendered only in create mode — edit mode keeps the existing `AppPermissionsEditor` with its impact preview) that lets admins add/remove permissions locally before submit, and wired `handleSaveApp` to forward the selected ids when creating. Existing edit path strips the field so the update payload remains unchanged. - `replit.md`: documented the new picker and POST /api/apps behavior. No impact preview is shown in the create-mode picker because a brand new app starts with zero users seeing it, so adding permissions cannot hide it from anyone. Code-review follow-up: tightened input validation so non-integer or non-positive `permissionIds` now return 400 with a clear error instead of being silently dropped by the previous filter. The legacy single-add endpoint already used this exact 400 message, so behavior stays consistent across both create and update paths. Verification: - `pnpm --filter @workspace/api-spec run codegen` passes. - `pnpm --filter @workspace/api-server` typechecks with no new errors (executive-meetings.ts errors are pre-existing and unrelated). - Ran the existing app-permission test suites (`app-permission-audit.test.mjs`, `app-permissions-crud.test.mjs`, `app-permissions-impact.test.mjs`) directly — all 16 tests pass. - Ran an e2e Playwright test (login as admin → Add app → pick a permission → save → verify the row shows 1 restriction → reopen and confirm the assigned permission). All steps passed. Follow-up proposed: automated tests for the new create-with-permissions endpoint behavior (#231). Replit-Task-Id: c229777c-4036-4a6a-b4cb-05ccc18f6c9b |
||
|
|
aaeb0a48a0 |
Task #230: Remove redundant top "+ subheading" chip in attendee cells
User feedback on Task #227: the top "+ عنوان فرعي" chip is visually redundant in every state (duplicates the trailing chip on empty / single-section cells, and feels like noise above the first heading once a heading exists). The trailing "+ عنوان فرعي" + after-section chips already cover every insertion point the user actually needs. Schedule cell (AttendeeFlow): - Removed the entire top "+ عنوان فرعي" chip JSX block (the gate `items.length === 0 || hasAnySubheading`, the testid `em-add-subheading-top-{addType}`, and surrounding comment). - Updated the surviving comments on `hasInlineInsert` and `renderPendingSubheadingLi` so they no longer mention the now-gone top chip — they reference only the after-section chip flow. - `hasAnySubheading` is kept (still used by person-row numbering and pending-person-ghost numbering). Per-section after-section chips (subheading- and person-branch) and the trailing "+ عنوان فرعي" / "+" buttons are unchanged. Tests (executive-meetings-attendee-insert-reorder.spec.mjs): - Removed the two top-chip tests: 1. "top chip on EMPTY cell inserts a subheading at slot 0" 2. "top chip is NOT rendered on a flat list with zero subheadings" - Replaced them with a single per-locale test: "top \"+ subheading\" chip is NEVER rendered (empty / person-only / subheading-only / mixed)". It seeds two meetings: an empty one (covers EMPTY state) and a three-section meeting that spreads attendees across the internal/virtual/external attendance-type groups so each AttendeeFlow renders in a different state (person-only, subheading-only, mixed). Asserts `em-add-subheading-top-*` has count 0 across the row, plus a sanity check on the mixed-row attendee count so the 0-count is meaningful. - After-section tests (between two sections / after empty section) and manage drag tests are unchanged. - Updated the file's top-of-file comment to mention #230 alongside #227. Verification: - 6 schedule tests in executive-meetings-attendee-insert-reorder PASS in en + ar (38.7s). - 6 regression tests in executive-meetings-attendee-subheadings PASS in en + ar (34.4s). - `tsc --noEmit` reports zero errors in executive-meetings.tsx. - Pre-existing dnd-kit RTL flake (Manage [ar/en]: drag handle reorders attendees inside the dialog) bounces locales between runs; unrelated to this diff (no changes to manage dialog drag flow, SortableAttendeeRow, or dnd-kit wiring). No deviations from the task spec. |
||
|
|
b507b33cad |
Add app-permissions impact preview before tightening an app's gate
Mirrors the existing role-permissions impact preview UX for app
permissions. Admins now see how many currently-visible users would lose
access before they add a permission requirement to an app, plus the
groups (via group_apps) that offset the loss because their members keep
access regardless.
Backend
- New endpoint POST /api/apps/:id/permissions/impact-preview in
artifacts/api-server/src/routes/apps.ts. Implements the same OR
semantics as getVisibleAppsForUser: a user "sees" an app if they hold
ANY required permission (direct or via a group role) OR they belong
to a group granted the app via group_apps. Admins are excluded from
counts since they always see every app. Short-circuits with
noChange:true when the candidate set equals the current set.
- OpenAPI schema (lib/api-spec/openapi.yaml): adds the path,
AppPermissionsImpactBody, AppPermissionImpactGroup,
AppPermissionsImpact. Regenerated lib/api-client-react bindings.
Frontend
- AppPermissionsEditor (artifacts/tx-os/src/pages/admin.tsx): debounced
(350ms) cancel-safe preview when a pending permission is selected,
warning banner with affected/visible counts and offsetting groups,
and a confirmation dialog when affectedUserCount > 0. Add button is
disabled while the preview is loading or errored to keep the warning
trustworthy.
- i18n keys added to en.json and ar.json under
admin.appPermissions.{impactTitle, impactLoading, impactError,
impactNone, impactSummary, impactViaGroups, confirmTitle, confirmBody,
confirmAction}.
Tests
- artifacts/api-server/tests/app-permissions-impact.test.mjs: 7 tests
covering noChange short-circuit, unrestricted-app tightening,
candidate that keeps an existing permission, group_apps offset,
unknown app (404), invalid payload (400), and admin-only enforcement.
All 18 app-permissions tests pass.
- E2E flow verified via runTest: admin login → /admin → Apps → edit
app → select permission → preview banner appears → Add → confirm
dialog → cancel without writing.
Out-of-scope (filed as follow-ups #228 and #229): listing the specific
affected user IDs in the preview, and warning when REMOVING a
permission broadens access.
No deviations from the task spec.
Replit-Task-Id: 8b2ff9ea-f95e-4bdb-b268-95b5d17154ca
|
||
|
|
40d290fd16 |
Update how subheading chips are displayed in meeting editor
Refactor the display of subheading chips to correctly render in empty cells and after sections. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 217126f7-0bc2-492b-9f16-a9bdc206d0a4 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/opt4zwD Replit-Helium-Checkpoint-Created: true |
||
|
|
d3ce0fe157 |
Task #227: Restructure attendee cell controls around section headings
User feedback on Task #220's UX (in Arabic): per-section "+ شخص هنا" chips felt redundant; the user wanted the "+ عنوان فرعي" controls placed at the TOP of each cell and after each section instead, and the "+ شخص هنا" chips removed entirely. Schedule cell (AttendeeFlow): - TOP "+ عنوان فرعي" chip renders BEFORE the first item of each AttendeeFlow group when the cell is empty OR when it already contains at least one subheading. For cells that only contain persons and no subheadings, the chip is intentionally hidden so the pre-#227 visual is preserved (per task's "no regression" clause). Testid: em-add-subheading-top-{addType}. insertAtIndex = items[0].i for non-empty cells, undefined for empty (so the commit appends at slot 0). - After-section "+ عنوان فرعي" chip renders AFTER every section that is followed by another section, including: * person-tail boundary (last person whose next item is subheading) * empty-section boundary (subheading whose next item is subheading) The trailing section is skipped — the trailing "+ عنوان فرعي" button below already covers that slot. Testid: em-add-subheading-after-{i}; insertAtIndex = i + 1. - Both "+ شخص هنا" chips removed (subheading-empty branch + person-tail branch). - Added renderPendingSubheadingLi helper for inline subheading ghost rendering at the clicked slot. Generalized hasInlineInsert to cover both person and subheading pending kinds. - Removed addPersonHereLabel/addPersonHereAriaLabel from AttendeeFlowSharedProps and from the schedule-side flowProps caller. Manage dialog: - Removed the per-section "+ شخص هنا" buttons from the SortableContext flatMap and the insertAttendeeAt helper. Reverted to plain state.attendees.map. Locales (ar + en): - Dropped executiveMeetings.schedule.addPersonHere/addPersonHereAria. - Dropped executiveMeetings.manage.attendees.addPersonHere/ addPersonHereAria. Tests (executive-meetings-attendee-insert-reorder.spec.mjs): - Removed the per-section "+ شخص هنا" chip test. - Added 4 new schedule tests (en + ar each): 1. Top chip on an EMPTY cell inserts a subheading at slot 0. 2. Top chip is NOT rendered on a flat list with zero subheadings (preserves the pre-#227 visual). 3. After-section chip inserts a subheading between two existing sections (BEFORE the next-section subheading). 4. After-section chip renders after an EMPTY section (consecutive subheadings) and inserts a subheading at the boundary. - Pre-existing drag tests #2 and #3 unchanged and still pass. - Pre-existing #4 (Manage [ar] mixed person+subheading drag) is reproducibly red on [ar] but green on [en]. The diff does NOT touch the manage dialog drag flow, the SortableAttendeeRow, or dnd-kit wiring — pre-existing RTL keyboard-sensor flake from #220. Architect review: PASS on first run; second-pass code-review verdict flagged scope alignment which has now been addressed (top chip on empty cell, no top chip on flat lists, after-section chip on consecutive subheadings, plus matching tests). |
||
|
|
bd78157781 |
Task #227: Restructure attendee cell controls around section headings
User feedback on Task #220's UX (in Arabic): the per-section "+ شخص هنا" chips felt redundant; the user wanted "+ عنوان فرعي" controls placed at the TOP of each cell and after each section instead, and the "+ شخص هنا" chips removed entirely. Schedule cell (AttendeeFlow): - Added a TOP "+ عنوان فرعي" chip that renders BEFORE the first item of each AttendeeFlow group when the group is non-empty (testid em-add-subheading-top-{addType}; insertAtIndex=items[0].i). - Added an after-section "+ عنوان فرعي" chip that renders AFTER the last person of each section that is followed by another subheading (testid em-add-subheading-after-{i}; insertAtIndex=i+1). The trailing section is intentionally skipped — the existing trailing "+ subheading" button already covers that slot. - Removed both "+ شخص هنا" chips (subheading-empty branch + person-tail branch). - Added renderPendingSubheadingLi helper for inline subheading ghost rendering at the clicked slot. Generalized hasInlineInsert to cover both person and subheading pending kinds. - Removed addPersonHereLabel/addPersonHereAriaLabel from AttendeeFlowSharedProps and from the schedule-side flowProps caller. Manage dialog: - Removed the per-section "+ شخص هنا" buttons from the SortableContext flatMap and the insertAttendeeAt helper. Reverted to plain state.attendees.map. Locales (ar + en): - Dropped executiveMeetings.schedule.addPersonHere/addPersonHereAria. - Dropped executiveMeetings.manage.attendees.addPersonHere/ addPersonHereAria. Tests: - Replaced the old "+ person here" test with two new schedule tests: (a) top "+ عنوان فرعي" chip inserts a subheading at slot 0, (b) after-section chip inserts a subheading between two existing sections (BEFORE the next-section subheading). - Both new tests run in en + ar. - Pre-existing drag tests #2 and #3 are unchanged and still pass. - Pre-existing #4 (Manage [ar] drag-reorder mixed person+subheading) is reproducibly red on [ar] but green on [en]. The diff does NOT touch the manage dialog drag flow, the SortableAttendeeRow, or dnd-kit wiring — this is a pre-existing RTL keyboard-sensor flake from #220. Architect review: PASS, no blocking issues. Minor optional follow-ups (extra edge-case test coverage) intentionally not addressed to keep scope tight to the user's request. |
||
|
|
40534b7c8f |
Add Playwright spec for /my-orders unmount-flush of pending deletes
Original task (#158): /my-orders has a useEffect cleanup that, on unmount, clears every still-running undo timer and immediately fires the deletions for whatever IDs were queued. This unmount-flush path was previously not covered by tests — only the "Undo" path was. Implementation: - New spec: artifacts/tx-os/tests/order-delete-flush-on-unmount.spec.mjs - Mirrors the test scaffolding (DB pool, user/order seeding, login, language init, toast locator) used by the existing order-undo-toast and order-clear-finished-undo specs. - Flow: seed a completed order, log in, go to /my-orders, click trash + "Yes, delete" to queue the deletion, then navigate away via the in-page Back button (which is wouter setLocation("/services") — an in-SPA route change, so MyOrdersPage actually unmounts and the cleanup useEffect runs). - Asserts both required outcomes from the task spec: * A DELETE /api/orders/:id request is observed during the navigation (with a 2xx response). * The order row is actually gone from the DB. - Additionally records the timestamp of the DELETE request and asserts it fires within 3s of the navigation (well under UNDO_WINDOW_MS). This guards against a regression that removes the cleanup but accidentally still passes because the natural 7s timer eventually fires anyway in the SPA — without the cleanup, the DELETE would arrive ~7s later and the assertion would time out. - Clean up the seeded order id from the afterAll list when the test successfully deletes it via the UI, so teardown does not try to re-delete a missing row. No production code changed; this is a pure test addition. Replit-Task-Id: 655f3d3b-5fc5-4a87-a754-76ec67148186 |
||
|
|
f6078a8968 |
Add Playwright tests for the bulk "Clear N finished" + undo flow on /my-orders
Task #157 asked for end-to-end coverage of the bulk-delete path on /my-orders, which goes through the same scheduleDelete() in artifacts/tx-os/src/pages/my-orders.tsx as the single-card delete but uses a single shared setTimeout that owns multiple order IDs and a different (i18n plural) toast title. None of that was exercised by the existing order-undo-toast.spec.mjs. New file: artifacts/tx-os/tests/order-clear-finished-undo.spec.mjs Three tests, all using the same DB-seeding + login pattern as the existing undo-toast spec: 1. English plural + Undo: seeds 3 finished orders, clicks "Clear 3 finished", confirms, asserts the plural toast ("3 orders deleted" => myOrders.clearedCount_other), clicks Undo inside the 7s window, then waits past UNDO_WINDOW_MS and asserts that NO DELETE /api/orders/:id requests went out, all 3 rows still exist in the DB, and all 3 cards are visible again. This is the core regression guard for the multi-id shared-timer logic. 2. English plural + timer expiry: seeds 1 completed + 1 cancelled order (to confirm both finished states are swept), clicks "Clear 2 finished", asserts the plural toast, does NOT click Undo, polls until both DELETE responses fire and both DB rows are gone. 3. Arabic singular bulk path: seeds 1 finished order, clicks the Arabic "Clear 1 finished" button, confirms, asserts the singular toast title. NOTE: when ids.length === 1 the bulk path falls through to myOrders.deleted ("تم حذف الطلب"), NOT clearedCount_one — so the test asserts actual current behavior and the comment explains why. Then Undoes and verifies the row stays. Deviation from task spec: the task said "Both the singular ('1 order deleted') and plural ('{count} orders deleted') toast titles are exercised at least once." The plural string is exercised, but the literal "1 order deleted" string (clearedCount_one) is unreachable in the current code — scheduleDelete uses myOrders.deleted for the single-id case. I tested the actual singular branch instead and filed follow-up #223 to either route the single-id bulk case through clearedCount_one or remove the dead translation key. Also filed follow-up #224 for the unused clearedPartial string (partial-failure toast for bulk clear). Verified locally: all 3 new tests pass via pnpm --filter @workspace/tx-os exec playwright test \ tests/order-clear-finished-undo.spec.mjs (3 passed in ~47s, exit 0). No existing files were modified. Replit-Task-Id: ba9e23a5-d8ff-4376-ab8c-d03d6a5f15e4 |
||
|
|
6132d2a00a |
Task #220: insert persons mid-list + drag-reorder attendees
Schedule grid (AttendeeFlow): - Per-section "+ شخص هنا" chip rendered AFTER the last PERSON of each section (a section ends right before the next subheading or at end-of-list), gated by hasAnySubheading. Clicking it opens a ghost row with insertAtIndex = lastPersonIdx + 1 so the new row lands at the TAIL of THAT section, before the next subheading. The chip belongs to the section ABOVE it, matching the user's spec. - Empty-section fallback chip kept on subheading rows: when a subheading is followed by another subheading or end-of-list, render the chip there so empty sections stay growable. Manage dialog: - Added _sid client-only field with genAttendeeSid() so dnd-kit ids stay stable across renames; stripped from save projection. - DnD via dnd-kit (DndContext / SortableContext / SortableAttendeeRow with GripVertical handle, keyboard + pointer sensors). New reorderAttendeesByDrag + sortableIds memo. - New insertAttendeeAt(idx) helper + per-section "+ شخص هنا" buttons injected as non-sortable siblings inside SortableContext after each subheading (sortableIds remains a pure _sid list so the keyboard sensor still works). Locales: addPersonHere + addPersonHereAria in both ar.json and en.json (schedule + manage scopes). Tests: - New e2e spec executive-meetings-attendee-insert-reorder.spec.mjs: (1) chip after last person of section inserts BEFORE the next subheading; asserts em-add-person-here-2 (subheading idx) does NOT exist in [A,B,SUB,C]; (2) keyboard DnD reorders 3 persons; (3) mixed person + subheading drag preserves kind. 6/6 pass in AR + EN. - Existing subheading e2e (Task #207): 6/6 still pass. Process notes: - First mark_task_complete REJECTED by code review for placing chip after subheading instead of after last person of prior section; this commit is the corrected pass. Architect re-review approved the semantic; only follow-up was the missing manage-scope addPersonHereAria locale key, which is added here. |
||
|
|
a0e12a15f7 |
Task #220: Insert persons mid-list + drag-reorder attendees
Original request: Add a per-section "+ شخص هنا" inline chip in the
attendees cell so users can splice a new person right after a
subheading without opening the manage dialog, and add drag-and-drop
reordering inside the manage dialog (with up/down arrows kept as a
keyboard fallback).
Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Attendee gets an optional client-only `_sid` (stripped by the
manage-save projection so it never crosses the wire).
- AttendeeFlow now renders a "+ شخص هنا" chip right after every
subheading row; clicking it opens the pending-add editor anchored
to that section's tail and threads `insertAtIndex` through to
`commitAddAttendee`, which splices + renumbers contiguously.
- Manage dialog wraps its attendee list in `DndContext` +
`SortableContext` (vertical strategy). Each row is now a
`SortableAttendeeRow` driven by `useSortable`; only the new
`GripVertical` handle is wired to dnd-kit listeners so inputs,
selects, chevrons and the delete button stay interactive.
- `reorderAttendeesByDrag(activeSid, overSid)`, memoised
`sortableIds`, and pointer + keyboard sensors live on the manage
section. `openEdit` and add-row helpers stamp `_sid` so each row
has a stable React key.
- AR/EN locale keys for the chip label/aria and drag handle aria
were already in place from the prior compaction.
Tests (new): artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
- 6/6 passing (3 scenarios × AR + EN):
1) "+ person here" inserts at the right slot after a subheading.
2) Drag-reorder via keyboard (Space + ArrowDown) inside the manage
dialog persists the new order.
3) Mixed person + subheading drag preserves `kind` and order
across the round-trip (architect-flagged coverage).
Verification:
- 6/6 new e2e pass (45.3s).
- 6/6 existing subheading e2e still pass (Task #207 untouched).
- tx-os typecheck clean for executive-meetings.tsx (admin.tsx errors
are pre-existing/unrelated codegen drift).
- Architect review: PASS — DnD wiring composed correctly, `_sid`
properly stripped from save payload, no security findings.
Follow-ups proposed: #221 (assert _sid never leaks to API) and #222
(quick-add chip between any two persons, not just after subheadings).
|
||
|
|
c03ea006e1 |
Add ability to insert and reorder people within meeting attendee sections
Adds functionality to insert new attendees at specific positions within sections and updates the attendee data structure and API calls to handle this. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: b9611a28-c97f-49a5-b84a-d63d07ef4426 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/piUNOmy Replit-Helium-Checkpoint-Created: true |
||
|
|
8d998bf8e8 |
Test the merge & current-meeting tint feature end-to-end (task #154)
Adds API + UI/e2e coverage for the cell-merge overlay introduced by task #152 on the executive-meetings schedule. Two new test files, no production code changes. API tests (artifacts/api-server/tests/executive-meetings-merge.test.mjs): - Setting a merge writes mergeStartColumn/mergeEndColumn/mergeText and the row round-trips back through GET ?date=. - Clearing with `merge: null` nulls all three columns while leaving the rest of the row intact (combined with a titleEn change in the same PATCH to prove only the merge fields move). - Invalid range start>end is rejected with 400 and the row is unchanged (also covers an unknown enum value). - mergeText is sanitized: <script>, onerror, javascript:, and <img> are stripped; visible text, <strong>, and the allowed inline color style (rgb(220,38,38)) survive — locks the contract that Tiptap-style formatting on the merged label is preserved. - A non-mutate user (executive_viewer role) gets 403 on PATCH merge and a follow-up DB SELECT confirms no fields changed. UI / e2e tests (artifacts/tx-os/tests/executive-meetings-merge.spec.mjs): - Two-tab realtime: separate browser contexts both open the same day; applying a merge in tab A makes the merged cell appear in tab B without a manual reload (relies on the existing `executive_meetings_changed` Socket.IO emit + client invalidator). - Hidden # column: with `em-schedule-cols-v1` localStorage flagging number as invisible, the row-actions kebab still mounts on the next visible cell and the Merge submenu is reachable. - Non-contiguous reorder: a row with a stored merge spanning meeting+attendees is loaded after reordering columns to [number, meeting, time, attendees]. The merged cell is NOT rendered (correct fallback) but the kebab still surfaces the Unmerge action, and clicking it clears all three merge columns in the DB. Both files clean up their own meeting rows + audit log entries + seeded users in afterAll/after. All 5 API tests + 3 Playwright tests pass against the running dev workflows. Follow-ups proposed: - #217 Show merged cells in the meeting PDF & archive views - #218 Test the merge popover in Arabic / RTL Replit-Task-Id: c696223a-209b-4111-9921-e0540e1e16a5 |
||
|
|
76f0dfacb4 |
Restart numbering for individuals after each subheading
Modify attendee numbering logic to reset at each subheading, ensuring proper sequence and display across different views and PDF outputs. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 17c29c3f-e55d-4765-875c-0387b6936812 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/piUNOmy Replit-Helium-Checkpoint-Created: true |
||
|
|
b23b8bdea9 |
Correctly save subheading type when adding new schedule items
Update executive-meetings.tsx to pass the 'kind' parameter in the onCommitAddAttendee function, ensuring subheadings are saved with the correct type. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 952ae299-fa70-40e4-b4e2-2013e3671e1e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/piUNOmy Replit-Helium-Checkpoint-Created: true |
||
|
|
97f251bab6 |
Task #149: Add automated mobile/touch test for reordering meetings on iPad
Adds a new Playwright spec `artifacts/tx-os/tests/executive-meetings-touch-reorder.spec.mjs` that exercises the iPad/touch path of drag-to-reorder on the Executive Meetings daily schedule — the path fixed by Task #141 (TouchSensor + `touch-action: none` on the row grip handle) but never covered by an end-to-end test. What the test does - Configures the spec's browser context with viewport 768x1024, hasTouch=true, isMobile=true so Chromium emulates an iPad and emits real TouchEvents that dnd-kit's TouchSensor can pick up. - Seeds two adjacent meetings on a unique far-future date via direct DB inserts (mirrors the strategy in executive-meetings-schedule-features.spec.mjs) and cleans them up in afterAll. - Logs in as admin via the UI, navigates to /executive-meetings, jumps to the seeded date, and turns on edit mode so the row grip handle renders. - Drives the touch gesture through CDP `Input.dispatchTouchEvent` (Playwright's high-level `page.touchscreen` only exposes `tap()`): touchStart on the first row's grip, hold still for 350ms (≥ task's 250ms / dnd-kit's 200ms activation delay), drag down past the second row in 12 sub-steps, touchEnd. - Asserts: (1) a POST /api/executive-meetings/reorder fires AND succeeds, (2) the request body's `orderedIds` reflects the swap, (3) the DB row daily_numbers + start/end times swap as the reorder endpoint specifies, (4) the visible DOM row order swaps, and (5) after a reload the new order persists. Wiring - The spec lives in `artifacts/tx-os/tests/` and is auto-discovered by the existing playwright config (`testMatch: /.*\\.spec\\.mjs$/`), so it runs as part of `pnpm --filter @workspace/tx-os test:e2e` with no config changes. Verification - `pnpm --filter @workspace/tx-os test:e2e -- executive-meetings-touch-reorder.spec.mjs` → 1 passed. - Re-ran the existing `executive-meetings-schedule-features.spec.mjs` (4 tests) to confirm no regression — all 4 still pass. Follow-ups - Proposed #214: Add iPad/touch test for reordering schedule columns (the file has a second DndContext for column-header drag that uses the same sensor stack and is currently untested on touch). Code review feedback - Reordered scrollIntoViewIfNeeded() to run BEFORE the boundingBox reads so any auto-scroll from bringing the grip on screen can't invalidate the touch coordinates we then dispatch via CDP. (Other comment about an unrelated opengraph.jpg change is platform-side, not part of this task's edits.) No deviations from the task spec. Replit-Task-Id: 3de9a478-e4a6-41db-8ed6-4feddccb3fd4 |
||
|
|
2bf2f3cd3a |
Task #147: Add structured permission-change audit (users, groups, apps)
Mirrors the existing role-permission audit pattern with a unified
`permission_audit` table capturing actor, target, prev/new id sets, and
timestamp written in the same transaction as the change.
Schema & API
- New `permission_audit` table (target_kind, target_id, change_kind,
actor_user_id, previous_ids[], new_ids[], created_at) with index on
(target_kind, target_id, created_at).
- Transactional audit writes in routes/users.ts (POST/DELETE roles,
PATCH groupIds), routes/groups.ts (PATCH + add/remove members for
users/roles/apps), routes/apps.ts (POST/DELETE permissions).
- Cross-entity mirroring: when group membership changes via a group
endpoint, a user.groups row is also written for each affected user
(and vice versa via PATCH /users), so each entity's history is
exhaustive regardless of which editor was used.
- Admin-only GET /users/:id/audit, /groups/:id/audit, /apps/:id/audit
with limit/offset/actorUserId/from/to filters and the same response
shape as role audit.
- OpenAPI types + codegen regenerated.
UI
- Reusable PermissionAuditHistory component in admin.tsx wired into
UserGroupsEditor, GroupDetailEditor (new "history" tab), and the
editing-app dialog. App history correctly resolves permission ids
(NOT roles) via useListPermissions.
- Bilingual i18n keys added under admin.{users,groups,apps}.history*
in en.json + ar.json.
Tests
- New backend tests: user-permission-audit, group-permission-audit,
app-permission-audit (14 cases — transactional capture, GET filters
& pagination, admin-only, 404 on unknown id, plus 2 new mirror
tests covering cross-entity audit visibility). All pass; 35
adjacent role/groups/users/audit-coverage tests still pass.
Notes
- replit.md updated to list `permission_audit` table.
- Restored opengraph.jpg (an unrelated stray binary diff).
- Code-review comments addressed: cross-entity asymmetry fixed via
mirroring; opengraph.jpg restored.
- Follow-ups proposed: timeline UI improvements, cascade audit on
delete/bulk paths, e2e UI test for History sections.
Replit-Task-Id: 4ba8c8de-8dfd-42c7-a3bc-964a1ed3a1a3
|
||
|
|
15d70be2af |
Task #207: custom subheadings inside executive-meeting attendee cells
Adds a new `kind` column (`varchar(16) NOT NULL DEFAULT 'person'`) on `executive_meeting_attendees` so users can interleave free-text section labels with person rows in a meeting's attendee list. Subheadings are excluded from the running attendee number and from the per-meeting attendee count surface, but reorder and delete identically to person rows. DB - New `kind` column in `lib/db/src/schema/executive-meetings.ts`, defaulting to `"person"`. Applied via direct SQL because `drizzle-kit push` trips on a pre-existing duplicate-row issue in `app_permissions` (already documented in replit.md). API (artifacts/api-server/src/routes/executive-meetings.ts) - `attendeeSchema` accepts `kind: z.enum(["person","subheading"])` with default `"person"`. - All 4 insert paths round-trip `kind`: POST create, PATCH meeting update (attendees replacement), PUT `/attendees`, and duplicate. - `pdf-renderer` mapper forwards `kind`. `PdfMeetingAttendee.kind` typed as `string | null` to match the DB column shape. Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx) - `AttendeeFlow` renders subheadings on a separate full-width row (`basis-full`, semibold, centered) and increments the running person index only for `kind === "person"`. Pending ghost row branches on `pendingKind`. - Inline "+ subheading" chip via `onStartAdd(type, "subheading")`. - Manage dialog: addSubheading button, subheading rows hide the title field, show a kind badge, and reorder/delete identically. Manage list summary count filters to person rows only (architect fix). - Print page renders subheadings as `.em-print-subheading` and skips them in the running counter. - New locale keys under `executiveMeetings.schedule` and `executiveMeetings.manage.attendees` in both `ar.json` and `en.json`. PDF - Subheadings print as `— label —` and never advance `personIdx`. Tests - New spec `executive-meetings-attendee-subheadings.spec.mjs` seeds mixed person+subheading rows and asserts (a) the subheading row renders, (b) numbering stays `1-`, `2-`, `3-` with a subheading wedged between persons, (c) zero-subheading meetings keep legacy numbering. Runs in both AR and EN. All 4 cases pass. Code review - Architect found one regression (Manage list summary count included subheadings) — fixed. |
||
|
|
89df2385a8 |
Task #207: custom subheadings inside executive-meeting attendee cells
Adds a new `kind` column (`varchar(16) NOT NULL DEFAULT 'person'`) on `executive_meeting_attendees` so users can interleave free-text section labels with person rows in a meeting's attendee list. Subheadings are excluded from the running attendee number and from the per-meeting attendee count surface, but reorder and delete identically to person rows. DB - New `kind` column in `lib/db/src/schema/executive-meetings.ts`, defaulting to `"person"`. Applied via direct SQL because `drizzle-kit push` trips on a pre-existing duplicate-row issue in `app_permissions` (already documented in replit.md). API (artifacts/api-server/src/routes/executive-meetings.ts) - `attendeeSchema` accepts `kind: z.enum(["person","subheading"])` with default `"person"`. - All 4 insert paths round-trip `kind`: POST create, PATCH meeting update (attendees replacement), PUT `/attendees`, and duplicate. - `pdf-renderer` mapper forwards `kind`. `PdfMeetingAttendee.kind` typed as `string | null` to match the DB column shape. Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx) - `AttendeeFlow` renders subheadings on a separate full-width row (`basis-full`, semibold, centered) and increments the running person index only for `kind === "person"`. Pending ghost row branches on `pendingKind`. - Inline "+ subheading" chip via `onStartAdd(type, "subheading")`. - Manage dialog: addSubheading button, subheading rows hide the title field, show a kind badge, and reorder/delete identically. Manage list summary count filters to person rows only (architect fix). - Print page renders subheadings as `.em-print-subheading` and skips them in the running counter. - New locale keys under `executiveMeetings.schedule` and `executiveMeetings.manage.attendees` in both `ar.json` and `en.json`. PDF - Subheadings print as `— label —` and never advance `personIdx`. Tests - New spec `executive-meetings-attendee-subheadings.spec.mjs` seeds mixed person+subheading rows and asserts (a) the subheading row renders, (b) numbering stays `1-`, `2-`, `3-` with a subheading wedged between persons, (c) zero-subheading meetings keep legacy numbering. Runs in both AR and EN. All 4 cases pass. Code review - Architect found one regression (Manage list summary count included subheadings) — fixed. |
||
|
|
fe736e3ff7 |
Task #146: Filter and paginate role permission history
Summary
- Backend: GET /api/roles/:id/audit now accepts limit (default 10, max 200),
offset, actorUserId (0 = no filter), and from/to (YYYY-MM-DD UTC). The
response is now a paginated envelope `{entries, totalCount, limit, offset,
nextOffset}` instead of a bare array.
- OpenAPI: lib/api-spec/openapi.yaml updated with the new params and a new
RolePermissionAuditList schema; client hooks regenerated via
`pnpm --filter @workspace/api-spec run codegen`.
- Frontend: RolePermissionHistory in artifacts/tx-os/src/pages/admin.tsx is
now self-contained — owns its own filter state, fetches the user list for
the actor dropdown, applies actor changes immediately, and validates date
inputs (rejecting invalid / inverted ranges).
- Pagination: switched to TRUE OFFSET PAGINATION. The first page comes from
React Query (so live cache invalidations after a save still refresh it),
and subsequent "Load more" clicks fetch `offset = nextOffset` imperatively
via getRolePermissionAudit() and append the rows to local state. There is
no client-side ceiling on how far back an admin can page; we simply stop
showing the button when nextOffset is null. A filtersKey effect resets the
appended pages whenever any filter (actor / from / to) changes so we never
serve overlapping or out-of-order rows.
- i18n: added missing keys in artifacts/tx-os/src/locales/{en,ar}.json
(historyEmptyFiltered, historyShowing, historyLoadMore, historyFilters.*,
historyErrors.*).
- Tests: artifacts/api-server/tests/role-permission-audit.test.mjs updated
to read the new envelope and now also covers offset-based pagination,
actorUserId filtering (including the "0 = no filter" semantic), and
date-range filtering (in-range / past-range / inverted / garbage). All 9
audit tests pass; tx-os typecheck clean.
- E2E: ran the testing skill end-to-end against /admin → role edit dialog →
history panel: created a fresh role through the UI, made 25 permission
writes, verified 10 → 20 → 25 pagination with no duplicates and correct
hide-on-end behaviour, filter-by-actor reset to first page, empty date
range showed empty state, inverted dates surfaced the validation error.
Drift / notes
- Pre-existing executive-meetings tests in the `test` workflow are still
failing — unchanged by this task.
- The Arabic language toggle isn't exposed in the page header in this build,
so the e2e Arabic step was skipped; locale strings are in place and the
test ids do not change with locale.
- Code review (initial pass) flagged a 200-row UI ceiling in the previous
"growing limit" approach. Replaced with true offset pagination (described
above) so admins can scroll back through arbitrarily long histories.
Code review follow-ups (round 2)
- Tightened parseRoleAuditUtcDate to reject impossible calendar days
(2024-02-31, 2025-13-01, etc.) instead of silently rolling forward.
- Aligned OpenAPI: actorUserId schema is now `minimum: 0` so the spec
matches the runtime "0 = no filter" contract; client regenerated.
- Added a targeted backend test that asserts impossible dates → 400.
Code review follow-ups (round 3)
- UX: when applied filters become invalid, the History list now hides the
stale entries and shows the "fix filters first" hint instead, and the
Load more button is hidden until filters are valid again. Avoids
presenting yesterday's results as the current view.
Code review follow-ups (round 4)
- Belt-and-braces: also reset the appended history pages when the first
page's totalCount + first-row id signature changes, so an external cache
invalidation (e.g. another save while the dialog is still open) cannot
leave appended pages out of sync with the refreshed first page.
Replit-Task-Id: cc3c9e83-921f-4f72-b443-79f95f6467b1
|
||
|
|
754a49591e |
Task #204: Make attendee group headers more prominent
Original ask: in the executive-meetings schedule's attendee cell, the
"Virtual / Internal / External Attendance" group labels (rendered above
each attendee group) read like stray attendee names — text-xs with em-dash
decoration. The user wanted them to stand out visually as section labels.
Changes
- artifacts/tx-os/src/pages/executive-meetings.tsx (AttendeeGroup):
* Header now renders text-sm font-bold, navy color (#0B1E3F), centered.
* Hairline underline (border-b border-[#0B1E3F]/15) with explicit
print:border-b print:border-gray-400 fallback so the underline is
preserved on print stylesheets that strip translucent borders.
* Em-dash decoration removed from the label.
* pointer-events-none + select-none so clicks/selection target the
attendee names, not the label.
* Added stable data-testid="em-attendee-group-header" for tests.
Tests
- New spec artifacts/tx-os/tests/executive-meetings-attendee-group-headers.spec.mjs:
* Seeds its OWN multi-group meeting (2 virtual + 3 internal) on a unique
far-future date per locale (offsets 1/2) — never collides with real
schedule data.
* Asserts BOTH expected localised headers appear (virtual + internal),
and that the external header is absent.
* Adds a regression guard: a single-group (all-internal) meeting on a
different unique future date renders ZERO group headers.
* Temporarily flips admin's preferred_language per-test (with try/finally
+ afterAll restore) so AuthContext does not override the locale via
i18n.changeLanguage on auth restore.
- Existing executive-meetings-edit-toggle.spec.mjs still passes (6/6).
- All 4 new tests pass.
Drift / deviations
- The original draft test lived inline in the toggle spec and skipped
because today's seed data has no multi-group meetings. Moved it to its
own spec with self-seeded data per code-review feedback so coverage is
deterministic, header assertions are tightened (require both expected
labels, reject the external one, not "any two of three"), and a
single-group regression guard is added.
|
||
|
|
24249bc511 |
Task #204: Make attendee group headers more prominent
Original ask: in the executive-meetings schedule's attendee cell, the
"Virtual / Internal / External Attendance" group labels (rendered above
each attendee group) read like stray attendee names — text-xs with em-dash
decoration. The user wanted them to stand out visually as section labels.
Changes
- artifacts/tx-os/src/pages/executive-meetings.tsx (AttendeeGroup):
* Header now renders text-sm font-bold, navy color (#0B1E3F), centered.
* Hairline underline (border-b border-[#0B1E3F]/15) with explicit
print:border-b print:border-gray-400 fallback so the underline is
preserved on print stylesheets that strip translucent borders.
* Em-dash decoration removed from the label.
* pointer-events-none + select-none so clicks/selection target the
attendee names, not the label.
* Added stable data-testid="em-attendee-group-header" for tests.
Tests
- New spec artifacts/tx-os/tests/executive-meetings-attendee-group-headers.spec.mjs:
* Seeds its OWN multi-group meeting (2 virtual + 3 internal) on a unique
far-future date per locale (offsets 1/2) — never collides with real
schedule data.
* Asserts BOTH expected localised headers appear (virtual + internal),
and that the external header is absent.
* Adds a regression guard: a single-group (all-internal) meeting on a
different unique future date renders ZERO group headers.
* Temporarily flips admin's preferred_language per-test (with try/finally
+ afterAll restore) so AuthContext does not override the locale via
i18n.changeLanguage on auth restore.
- Existing executive-meetings-edit-toggle.spec.mjs still passes (6/6).
- All 4 new tests pass.
Drift / deviations
- The original draft test lived inline in the toggle spec and skipped
because today's seed data has no multi-group meetings. Moved it to its
own spec with self-seeded data per code-review feedback so coverage is
deterministic, header assertions are tightened (require both expected
labels, reject the external one, not "any two of three"), and a
single-group regression guard is added.
|
||
|
|
18f334c6aa |
Task #203: Hide attendee number when only one attendee in a group
Original ask (in Arabic): in the executive-meetings schedule, drop
the leading "1-" prefix when an attendee group (virtual / internal /
external) has exactly one entry; keep "1-, 2-, 3-, ..." numbering
when the group has 2+ entries. Behaviour must be symmetric in
ar/en and rtl/ltr.
What changed
- artifacts/tx-os/src/pages/executive-meetings.tsx (~3313):
wrapped the index `<span data-testid="em-attendee-index-<i>">`
in `{items.length > 1 && (...)}`. Because the renderer is called
per attendance group, the rule applies independently to each
group: e.g. a meeting with 1 virtual + 3 internal still numbers
the internal list 1-, 2-, 3- while the lone virtual attendee
shows no prefix.
- The pending "+ ghost" add row at line ~3341 was intentionally
left unchanged per the task spec — once committed the new
attendee will live in a >=2 group.
Tests
- artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs:
the previous parameterised "same visual line" test asserted on
`em-attendee-index-*` boundingBox unconditionally, which would
break against single-attendee rows after this change. Split it
into two parameterised specs per locale (en, ar):
* multi-attendee group: locator filtered by
`has: em-attendee-index-*` — still asserts same-line layout
in view and edit modes (regression guard for #173/#175).
* single-attendee group: locator filtered by
`hasNot: em-attendee-index-*` — asserts the index span has
count 0 inside the row while the name remains visible.
Extracted the shared schedule-setup into `gotoSchedule()`. All
6 tests in this file pass locally (~1.9m).
Code review: PASS (architect).
Out of scope: visual styling of the prefix; data model; pending
ghost row behaviour; bulk-clear / bulk-delete from #198.
|
||
|
|
4ace4469eb |
Add Playwright keyboard-editing tests for executive-meetings schedule
Task #139: cover Enter/Esc/Tab keyboard editing of time, title, and attendee inline editors with end-to-end tests that seed via DB and log in via UI as admin/admin123. What this adds - artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs with 7 tests: * Tab into time cell + Enter opens edit + valid times save via Enter (asserts PATCH on /api/executive-meetings/:id and DB row updated) * Time cell Esc restores originals and sends no PATCH * Title cell Enter opens edit and Enter saves via PATCH * Title cell Esc restores original and sends no PATCH * Attendee cell Enter opens edit and Enter saves via PUT on /api/executive-meetings/:id/attendees * Attendee cell Esc restores original and sends no PUT * Tab from title cell reaches the time cell in the same row Implementation notes / deviations - Each test seeds its own future-dated meeting (and attendee where needed) directly through the pg pool with a unique date, then cleans those rows in afterAll — same pattern as the existing schedule-features spec. - After every save the page invalidates the day query and refetches; assertions wait for that GET to land before reading the DOM, which removed an early flaky-timing failure. - The title save test was originally written to do select-all + type. We discovered (via instrumented PATCH inspection) that the EditableCell writes to title_ar vs title_en based on the user's preferredLanguage, not the visible UI direction — admin's preferred language is ar, so saves go to title_ar even when the schedule is rendered LTR. The test now appends a unique suffix and asserts against whichever column actually received the saved HTML, mirroring the schedule-features formatting test. The underlying language-write mismatch is captured as a follow-up. - All 7 tests pass locally (~58s total). The pre-existing failures in the `test` workflow are unrelated PDF tests, not from this work. Code-review feedback applied - Removed two unrelated stray files (artifacts/tx-os/nohup.out and artifacts/tx-os/public/opengraph.jpg) that had no code references. - Scoped both attendee selectors under the seeded row's testid (em-row-:meetingId) so the locator stays unambiguous even if another test ever shares a date. Follow-ups proposed - #201 (test_gaps): Shift+Tab + cross-row + ghost-row keyboard tests - #202 (tech_debt): Save edits to the column matching the visible UI Replit-Task-Id: cbd9619c-1475-43c6-998e-163e8e6ec94a |
||
|
|
afa79fb618 |
Hide attendee number when only one person is listed
Conditionally render the attendee index span element only when the number of items in the list exceeds one. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 6a614115-a39d-4b50-ac9b-9d97294fc18d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fEDT27c Replit-Helium-Checkpoint-Created: true |