2e7152fef78159089a956ad62ebf61e1a4efc81b
105 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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).
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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.
|
||
|
|
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 |
||
|
|
b1f2bab1cd |
Task #138: Show a clear message when meeting end time is before start time
Original ask
- In the executive-meetings schedule, when an admin edits a meeting's
inline time and types end < start, the change was silently discarded.
- Surface a localized toast for both the client-side guard and the
server's 400 validation, and keep the cell in edit mode with the
user's draft preserved so they can fix the typo without retyping.
What changed
- artifacts/tx-os/src/pages/executive-meetings.tsx
- TimeRangeCell.save now defers setEditing(false) until after the
server PATCH succeeds. On a thrown TimeOrderError it stays in edit
mode (preserving the typed draft) and refocuses the start input.
On any other failure it rolls back to the saved values as before.
- The parent saveTimes callback inspects the API error message; when
it matches the server's "startTime must be <= endTime" validation,
it shows the same localized timeOrderError toast (instead of the
raw English error string) and rethrows a tagged Error with
name "TimeOrderError" so TimeRangeCell can react.
- artifacts/tx-os/src/locales/en.json + ar.json
- Updated the existing executiveMeetings.schedule.timeOrderError
keys to the exact wording from the task spec:
EN: "End time is before start time"
AR: "وقت النهاية قبل وقت البداية"
Notes / non-changes
- The client-side guard at the top of TimeRangeCell.save already
emitted the toast and returned without exiting edit mode, so it only
needed the new copy. The change to defer setEditing(false) avoids a
race where the !editing useEffect would wipe the draft to startSaved
during the network round-trip on the server-side error path.
- No new i18n keys were added; existing keys were repurposed with the
spec's exact wording.
Verification
- pnpm tsc on tx-os: no new TypeScript errors in executive-meetings.tsx
(pre-existing errors in admin.tsx / use-notifications-socket.ts are
unrelated to this task).
- e2e Playwright test: created a meeting at 10:00–11:00, opened the
inline editor, set 09:00 / 08:00 and clicked save → destructive
toast "وقت النهاية قبل وقت البداية" appeared, inputs stayed visible
with the typed values intact. Corrected end to 10:00 and saved →
cell exited edit mode showing 09:00 – 10:00 and the API persisted
the new times.
Replit-Task-Id: f701faa8-02a9-4389-b130-92e522744128
|
||
|
|
0fe28fe16d |
Update toggle labels and restore image
Update locale keys for toggle functionality from 'editToggle' to 'saveToggle' and revert 'opengraph.jpg' to its previous state. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 05fe07ec-5152-4ad6-985f-c842d7501206 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 |
||
|
|
6abc60deaa |
Task #198: Edit→Save toggle + bulk-clear attendees + multi-select delete meetings
Three executive-meetings polish features on the Schedule view: 1. Edit-mode toggle now flips its label (Edit↔Save) and icon (Pencil↔Check) so a second click reads as "I'm done editing." 2. New-/edit-meeting dialog gained a "Remove all attendees" button next to "+ Add attendee" with a confirm prompt; only renders when ≥1 attendee is present. 3. Per-row select checkboxes (only in edit mode) drive a tri-state "select all" checkbox in the schedule header AND a floating bulk toolbar that appears only when ≥1 row is selected. The toolbar shows "Selected N of M" with "Delete selected" + "Clear selection". Single confirm wipes all checked meetings. Implementation notes: - Bulk-select checkbox is rendered as an absolute overlay on the first visible non-merged cell of every row (and on the merged <td> for merged rows) instead of being inlined inside the # cell. This (a) preserves the # cell's grip handle so dnd-kit drag still works, (b) keeps the checkbox reachable when users hide # via the column customizer, and (c) keeps it reachable on rows whose merge swallows the leading cells. - Tri-state "select all" is rendered as an absolute overlay on the first visible <th> in <thead> (typically # but falls back to the next visible header when # is hidden). - Floating bulk toolbar is gated on `selectedMeetingIds.size > 0` per spec — it disappears entirely with zero selection. - Selection clears on date change, edit-mode-off, post-bulk-delete, AND on every successful day refresh (detected via meetings array reference change from useQuery), so a refresh that returns the same ids cannot leave a stale selection. - Locale text added to ar.json/en.json for saveToggle.*, bulk*, removeAll, schedule.bulkSelectRow. Tests: - New spec executive-meetings-bulk-actions.spec.mjs (5 tests): edit toggle flip, dialog remove-all-attendees, multi-select delete with DB verify + tri-state header indeterminate/unchecked state assertions, hidden-# overlay survival, merged-row overlay survival. All pass. - Verified no regressions in executive-meetings-edit-toggle (4/4), -row-actions-menu (2/2), -schedule-features (4/4, including drag-row reorder), -manage-create (1/1). Code review (architect, 3 rounds): - Round 1 flagged checkbox-in-#-cell breaks when # is hidden → fixed by lifting to absolute overlay. - Round 2 flagged merged rows skipped overlay → fixed by sharing the overlay JSX between renderCell (first unmerged cell) and the merged <td>, plus added the merged-row e2e regression test. - Round 3 (validation) flagged: (a) toolbar visible without selection, (b) tri-state should be in <thead>, (c) selection should reset on day refresh. All three addressed; tests updated. Pre-existing failures unrelated to this task: 2 PDF tests in api-server (Tasks #172/#179) — untouched. |
||
|
|
0062430b31 |
Show deleted entity name beside target id in admin Audit Log
Task #133: Admin Audit Log rows now display the deleted item's human-readable name next to its `<type> #<id>` target reference without admins having to expand the JSON metadata. Changes - artifacts/tx-os/src/pages/admin.tsx - New helper `forceDeletedEntityName(entry, lang)` that pulls a localized display name from a force-delete row's metadata. Priority: nameEn/nameAr/displayNameEn/displayNameAr (lang-aware) > plain `name` > `@username`. - `AuditLogRow` renders an additional muted "Target: <type> #<id> · <name>" line beneath the summary for force-delete entries that expose a name. When no summary is generated, the existing target fallback line is upgraded to include the name as well. - artifacts/tx-os/src/locales/en.json, ar.json - New `admin.audit.targetWithName` key with English and Arabic translations. Behavior - Names respect the active language (Arabic preferred when lang=ar, with graceful fallback to the other locale, then `name`, then `@username`). - Non-deletion rows and deletion rows whose metadata lacks a name are unchanged — no extra line is rendered, so there is no regression for existing entries. Verification - `pnpm exec tsc --noEmit` in artifacts/tx-os passes after running `pnpm --filter @workspace/api-spec run codegen`. - E2E test (Playwright) confirms the new line renders for both a service.force_delete row (English-only metadata) and an app.delete force row (English + Arabic metadata), and that the Arabic UI surfaces the Arabic name when present and falls back to the English name when not. Follow-ups proposed - #194 Persist user display names in user.delete audit metadata. - #195 Log every service deletion, not only forced ones. Replit-Task-Id: cd312541-1c90-4849-afd6-e6757aedfe06 |
||
|
|
5c21bf9737 |
Improve test to accurately verify meeting row actions menu
Refactor e2e test assertion for executive meeting row actions menu to use a more robust check for kebab visibility and count. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4624aae4-b60d-45dd-aeaa-32ff564cc1b9 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/wd2kz0e Replit-Helium-Checkpoint-Created: true |
||
|
|
311d59ccad |
Task #191: Consolidate row-cell overlay icons into single kebab menu
The schedule's per-row affordances (delete, row color, merge cells)
used to render as three separate hover-revealed icon buttons stacked
inside the same narrow cell, which collided visually on iPad and
small screens. Replaced them with a single MoreVertical kebab trigger
that opens a Radix popover with three views (main / color / merge),
plus a "Back" affordance to return from sub-views to the main menu.
Implementation:
- New RowActionsMenu component (artifacts/tx-os/src/pages/executive-meetings.tsx)
rendered once per row, anchored at the trailing-top of the first
visible cell (matching the previous overlay anchor logic). Resets
to "main" view on close.
- Old DeleteRowButton, RowColorPicker, and MergeMenu components removed.
- Existing testids preserved on the popover items so external tests
still target em-delete-row-{id}, em-row-color-trigger, and
em-merge-trigger-{id}. New testids added: em-row-actions-{id} for
the kebab trigger, em-row-actions-back for the Back button.
- Locale keys added in ar.json/en.json: rowActions.label, rowActions.back.
- New imports: MoreVertical, ChevronLeft, ChevronRight (RTL-aware Back arrow).
- Merged-cell branch also uses RowActionsMenu so merge/unmerge stays
reachable when the # column is hidden.
Tests:
- New regression spec: executive-meetings-row-actions-menu.spec.mjs
exercises the full menu flow (open → color → back → main → merge → escape).
- Existing executive-meetings-edit-toggle.spec.mjs and
executive-meetings-schedule-features.spec.mjs continue to pass —
legacy testids remain absent in view mode and present inside the
popover when opened in edit mode.
Pre-existing test failures (NOT caused by this change):
- "non-mutate user (executive_viewer)" spec fails because seeded user
"ahmed" is missing from the dev DB (parallel task #131 drizzle work).
- TS errors in admin.tsx tracked by other tasks (api-client codegen
out of sync).
Follow-up proposed:
- #192: show row color + merge previews inline in the main menu items.
|
||
|
|
6908207df5 |
Show dependency counts inline in admin lists (Task #128)
Surfaces the dependency counts (already returned by the admin list endpoints since Task #96) directly in the Apps, Services, and Users admin panel rows so admins can see usage at a glance — no need to open the delete dialog to find out. Changes: - artifacts/tx-os/src/pages/admin.tsx: - Apps panel rows now render a small bullet-separated subtitle line under the route showing non-zero groupCount / restrictionCount / openCount (data-testid="app-counts-<id>"). - Services panel rows render an inline "N orders" line under the price when orderCount > 0 (data-testid="service-counts-<id>"). - Users panel rows render a bullet-separated subtitle under the displayName showing non-zero noteCount / orderCount / conversationCount / messageCount (data-testid="user-counts-<id>"). - Style mirrors the existing GroupsPanel inline counts row (text-[11px] muted-foreground, flex flex-wrap, "•" separators). - Zero counts are filtered out so empty rows stay clean. - artifacts/tx-os/src/locales/{en,ar}.json: - Added admin.apps.counts.{groups,restrictions,opens} - Added admin.services.counts.orders - Added admin.users.counts.{notes,orders,conversations,messages} - lib/api-client-react/dist + tsbuildinfo: regenerated stale composite build output so the count fields on UserProfile / App / Service schemas (added in Task #96) are visible to the tx-os typecheck. No source change in lib/api-client-react. Verification: - tsc --noEmit passes cleanly for artifacts/tx-os. - End-to-end browser test confirmed: admin sees inline counts in Apps, Services, and Users; rows with zero deps render no counts row; Arabic/RTL layout still works. Replit-Task-Id: 31d3e38d-f611-4d5e-9cfa-823326495328 |
||
|
|
4e9cb38ab1 |
Update meeting scheduling button text for clarity
Correct the text for the "Add meeting" button in both Arabic and English locale files to remove unnecessary characters. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: b75246ad-35b1-43b5-a2df-48c01dd0337b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/wYObB6M Replit-Helium-Checkpoint-Created: true |
||
|
|
b4d66bc0f2 |
Task #114: Readable audit log summaries for delete/update entries
- artifacts/api-server/src/routes/groups.ts:
- Added loadSubResourceNameFields() to fetch username / appSlug+nameEn+nameAr / roleName when adding or removing a group sub-resource.
- POST/DELETE /groups/:id/:kind/:targetId now persist these names alongside the id in audit metadata. Best-effort lookup on DELETE so a missing linked record does not break the action.
- Removed unused SUB_TABLE constant.
- artifacts/tx-os/src/pages/admin.tsx:
- formatAuditSummary now renders human-readable lines for:
- app.update rename (changes.nameEn/nameAr/slug from→to)
- group.update rename (previousName, when only one field changed)
- group.user/app/role.add/remove (prefer enriched name fields, fall back to *ById locale keys with #id when only the id is known)
- settings.update registrationOpen toggle ("Opened/Closed public registration", with optional "with N other change(s)" suffix)
- Helper linkedAppName() shared by the app branches.
- Raw JSON metadata remains available via the existing expand toggle.
- artifacts/tx-os/src/locales/{en,ar}.json:
- Added admin.audit.summary keys for app.rename, group.rename, group.{user,app,role}{Add,Remove}/{Add,Remove}ById, settings.registrationOpened/Closed/OpenedWith/ClosedWith in both English and Arabic.
Verification:
- Backend metadata enrichment validated end-to-end (group create, user/app/role add+remove, group rename, settings toggle) via a temporary script — all rows persist the new name fields.
- Browser e2e test logged in as a freshly created admin, exercised the same flows, opened the Audit log panel, and confirmed each row renders the readable summary (no #id placeholders) and that the raw JSON pane still expands and contains 'username'.
Notes / drift:
- The task brief listed many actions (user.delete, role.delete, app.delete, app.create, etc.). The group sub-resource actions and rename / registration toggle branches were the ones that previously rendered as opaque "#id" text and are addressed in this task. Top-level user/app/role delete already had decent metadata; further enrichment proposed as a follow-up so it can be reviewed independently.
Replit-Task-Id: 16fe1927-3329-4c15-b1ae-5fe10869aed0
|
||
|
|
7877cb173f |
Schedule attendees: force number+name onto the same visual line
Task #175. Follow-up to #173. The first fix added `whitespace-nowrap` on each attendee `<li>`, but users still saw the index span (`1-`, `2-`) stacked above the name — even with very short names like "رياض" / "محمد" that obviously fit on one line. Two screenshots (before and after the merge) showed the same stacked layout, ruling out narrow-column wrapping. Root cause: attendee names are saved as tiptap HTML such as `<p>محمد</p>`. Inside the inline-block EditableCell shell (and even inside the plain view-mode `<span>`), the default block-level `<p>` with its 1em top/bottom margins forced the name onto its own visual row beneath the index span. `whitespace-nowrap` cannot pull a block child back onto the parent line. Fix (artifacts/tx-os/src/pages/executive-meetings.tsx): - Each attendee `<li>` is now `inline-flex items-baseline whitespace-nowrap` so the index span and the name wrapper become flex children that structurally cannot break apart. - The view-mode `<span>` (plain dangerouslySetInnerHTML) gets `[&_p]:inline [&_p]:m-0` — tiptap `<p>` renders as inline with no margins. - The editable EditableCell wrapper gets the more-scoped `[&>span_p]:inline [&>span_p]:m-0`, which matches only the view-mode shell `<div> > <span> > <p>` and deliberately does NOT match the editing shell `<div> > <div(border)> > EditorContent`, so pressing Enter inside the editor still creates a real new paragraph. Tests (artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs): - New regression spec asserts that the index span and the name wrapper share the same vertical center (within 8px) for the first attendee in BOTH view mode and edit mode. Without the fix the centers differ by a full line height (~20px+). - The new spec is parameterised over `tx-lang` so it runs once for English (LTR) and once for Arabic (RTL) — the bug originally surfaced on the Arabic schedule, so RTL coverage matters. - The new spec self-skips (rather than fails) if the schedule has no attendees, so an empty environment doesn't masquerade as a layout regression. - All passing. Out of scope (unchanged): grouping/sorting, index format, multi- group Virtual/Internal/External rows, pending +Add ghost row, other EditableCell call sites (title, time, notes, manage tab). |
||
|
|
1e221754b3 |
Adjust time display to show 24-hour format left-to-right
Modify time formatting to use 24-hour clock and enforce left-to-right display for time ranges in executive meetings. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 78b494e1-a814-4fb6-8646-6932627fdbab Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/2GpAnn7 Replit-Helium-Checkpoint-Created: true |
||
|
|
11aaaf2abe |
Replace executive-meetings print-to-PDF with server-side PDF generator
The "Download PDF" button on the executive-meetings page now hits a real
backend endpoint that returns a true PDF (no more browser print dialog),
respects each user's font preferences (family, size, weight, alignment)
with proper Arabic RTL shaping, and archives every download.
The renderer maps each saved fontFamily ("system", "Cairo", "Tajawal",
"Noto Naskh Arabic", "Amiri") to a concrete pair of bundled font files
so the chosen family genuinely changes the embedded glyphs — Cairo and
Tajawal pick Noto Sans Arabic, the Naskh-style families and the system
default pick Noto Naskh Arabic, and Latin glyphs render in DejaVu Sans
across the board. Headers, body cells, and footer all flow through the
same script-aware font selection.
Backend (artifacts/api-server)
- New GET /api/executive-meetings/pdf?date=&lang= route in
src/routes/executive-meetings.ts that fetches the day's meetings +
attendees, renders a PDF, uploads it to object storage, writes an
executive_meeting_pdf_archives row (date, generated_by, byte_size,
storage_url), and streams the file back inline.
- New src/lib/pdf-renderer.ts using pdfkit + bidi-js with bundled
Noto Naskh Arabic and DejaVu Sans fonts in assets/fonts/.
- Added byte_size column on executive_meeting_pdf_archives (also in
lib/db schema) and rebuilt lib/db.
- Added ambient types for bidi-js; installed @swc/helpers to satisfy
fontkit at runtime.
- build.mjs now copies pdfkit's data/ folder (Helvetica.afm, etc.)
into dist/data so the bundled server can construct PDFDocument.
Frontend (artifacts/tx-os)
- PdfSection in src/pages/executive-meetings.tsx now renders a single
"Download PDF" button that fetches the endpoint, builds a Blob, and
downloads it. Removed the print/archive-creation buttons.
- Archive list shows a Download button for new /objects/... rows and a
read-only "Legacy snapshot" badge for older print: rows.
- Added byteSize on PdfArchive + size formatting; updated en/ar locales.
Tests
- New test "PDF GET /executive-meetings/pdf returns a real PDF and
archives it" in tests/executive-meetings.test.mjs covers: bad-date
400, unauthenticated 401, real %PDF body + content-type/disposition,
archive row with byteSize/generatedBy/filePath, empty-day handling,
and font-family mapping (Cairo embeds NotoSansArabic; Noto Naskh
Arabic embeds NotoNaskhArabic).
- All 23 executive-meetings tests pass.
Rebase
- Rebased onto main-repl/main (
|
||
|
|
37255b7eaa |
Update the website's shared image
Replace the existing shared image with an updated version. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 2b1d3400-ee06-468b-9b14-45d90e2d90bc Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/ZpWlWse Replit-Helium-Checkpoint-Created: true |
||
|
|
5f61ab7bf1 |
Send executive-meeting notifications via email + in-app alerts
Wired the Executive Meetings module to actually deliver notifications
when meeting/request/task events happen, instead of just storing
scheduled-notification rows.
Backend (artifacts/api-server):
- New helper `lib/executive-meeting-notify.ts`:
- `recordExecutiveMeetingNotifications` inserts rows into both
`executive_meeting_notifications` (the page's Notifications tab)
and the global `notifications` table (the bell), inside the
caller's transaction. Self-notifications are excluded; recipients
are deduped.
- `broadcastExecutiveMeetingNotifications` emits Socket.IO
`notification_created` to each recipient's `user:${id}` room and
one `executive_meeting_notifications_changed` global event. Called
after the surrounding transaction commits.
- `getUserIdsForRoleNames` resolves role holders via direct
`user_roles` and indirect `group_roles` -> `user_groups`.
- `sendExecutiveMeetingEmail` is a best-effort side-channel that
logs an outbox entry when SMTP_HOST is unset (no nodemailer
dependency added yet — see follow-up task).
- `getUserDisplay` resolves bilingual display names with username
fallback for use in notification titles/bodies.
- `routes/executive-meetings.ts` wired notifications into:
- POST /executive-meetings (notify approvers — meeting_created)
- POST /executive-meetings/requests and POST
/executive-meetings/:id/requests (notify approvers + email outbox
— request_submitted)
- PATCH /executive-meetings/requests/:id (notify requester —
request_approved/rejected/needs_edit; if approved with assignee,
notify assignee — task_assigned)
- POST /executive-meetings/tasks (notify assignee — task_assigned)
- PATCH /executive-meetings/tasks/:id (reassign -> task_assigned;
completion -> task_completed to original requester + previous
assignee)
Frontend (artifacts/tx-os):
- `hooks/use-notifications-socket.ts` listens for
`executive_meeting_notifications_changed` and invalidates the
notifications/requests/tasks query keys so the page re-fetches in
real time.
- `locales/en.json` + `locales/ar.json`: replaced placeholder intro
with the real description and added type labels for the seven new
notification types.
Verification:
- Restarted the API server (clean build).
- HTTP integration test: logged in as admin, created a meeting,
submitted a request, approved the request — confirmed
`executive_meeting_notifications` and `notifications` rows were
inserted with correct counts (7 admins, actor excluded), the
request_submitted email outbox log fired with bilingual subject/
body and 6 deliverable email recipients, and self-notifications
were correctly suppressed when actor == requester == reviewer.
No deviations from the original plan. Email delivery, per-user
notification preferences, and automated tests for the fan-out logic
are tracked as follow-ups.
Replit-Task-Id: accea784-663c-4b63-a492-8e20d648eb4c
|
||
|
|
9b3e320c78 |
Add Playwright tests for the My Orders cancel/delete undo toast
Original task (#103): cover the 7-second "Undo" action that appears after cancelling or deleting an order on /my-orders, since the existing order-place-cancel spec only verified the post-cancel state and never exercised the undo path. What was added: - New spec at artifacts/tx-os/tests/order-undo-toast.spec.mjs with two cases: 1. English: place a fresh pending order via the services modal, cancel it from the in-card confirm dialog, click "Undo" inside the toast within the 7-second window, then assert the restore PATCH /api/orders/:id/status succeeds, the timeline + Cancel button reappear in the card, and the DB row is back to "pending". 2. Arabic: seed a "completed" order directly in the DB, click the trash icon + "نعم، احذف", then click "تراجع" in the toast. Verifies that NO DELETE /api/orders/:id ever fires (page-level request listener), the order row still exists in the DB after the original 7s window has elapsed, and the card stays visible with the مكتمل status pill. - Both English and Arabic toast labels are exercised, satisfying the bilingual requirement. Implementation notes / deviations: - The spec is automatically picked up by the existing test:e2e config (testMatch: /.*\.spec\.mjs$/), so no playwright.config or package.json changes were needed. - DB seeding/cleanup mirrors the conventions in artifacts/tx-os/tests/order-place-cancel.spec.mjs (same bcrypt hash for TestPass123!, same afterAll teardown of orders/notifications/user_roles/users). - While wiring the test I discovered the shadcn Toaster's <li> root has no role="status" on this Radix version, so role-based selectors miss it. The spec uses `li[data-state="open"]` (Radix's data attribute) scoped to the most recent toast — explained in a comment in the file. Verification: - pnpm exec playwright test tests/order-undo-toast.spec.mjs → 2 passed. - Re-ran together with the existing order-place-cancel spec → 4 passed, no regressions. Follow-ups proposed (#157, #158): bulk "Clear N finished" + undo coverage, and the unmount-flushes-pending-deletes path. Replit-Task-Id: bd1c5067-e994-4299-b3a6-855ffb45ed71 |
||
|
|
539f11e25d |
Task #152: cell-merge + current-meeting tint on EM schedule
Adds two related features to the executive-meetings schedule: 1. Current-meeting tinting — meeting/attendees/time cells of the row matching the current time get a soft wash of the user's highlight color (~13% alpha), keeping their original text color and the existing colored ring. The # cell stays solid white/red. 2. Cell-merge overlay — editors can merge "meeting+attendees", "meeting+attendees+time", or the whole row into a single free-text cell. Stored in the DB so all viewers see the same overlay; the originals (titleAr/En, attendees, start/endTime) stay untouched and are restored on Unmerge. Implementation notes - DB: 3 nullable columns added to executive_meetings (merge_start_column, merge_end_column, merge_text). Applied via direct ALTER TABLE because pnpm --filter db run push is blocked by the pre-existing app_permissions duplicate (Task #148, surfaced as the new follow-up #156). Schema file kept in sync. - API: meetingPatchSchema accepts an optional `merge` field (`null | {start,end,text}`) with start<=end ordering and server-side sanitizeRichText on the text. Reuses the existing PATCH route. - Frontend: new MergeMenu component + canonical-order range resolution so merges still render when boundary columns are hidden, and gracefully degrade to unmerged rendering (without losing DB state) when column reordering makes the visible span non-contiguous. Unmerge stays available whenever a stored merge exists, even in degraded state, and existing mergeText is preserved on re-merge. - i18n: en + ar keys under executiveMeetings.merge.* Out of scope (deferred to follow-ups #154 / #155, per task brief) - API + e2e test coverage for the merge endpoint and UI flows - Realtime emission for executive-meetings mutations (no existing emit on this surface; covered by #155) Validation - TypeScript compiles cleanly across api-server + tx-os. - API tests: 108/110 pass — the 2 failures are the pre-existing app_permissions-unique cases (Task #148, follow-up #156), unrelated to this change. - Architect code review approved after addressing hidden-column fallback, non-contiguous reorder degradation, and Unmerge-availability edge cases. |
||
|
|
2ffd63832a |
Add e2e tests for the receiver-side incoming-orders flow
Task #102: Adds Playwright UI coverage for the receiver side of the service-orders flow, mirroring the existing API-level matrix in artifacts/api-server/tests/service-orders.test.mjs. What's added - artifacts/tx-os/tests/order-receiver-flow.spec.mjs with two tests: 1) "receiver claims, prepares, completes — requester sees live status updates": seeds a requester (role "user") and a receiver (role "order_receiver"), runs each in its own browser context, has the requester place an order via /services, then has the receiver claim it from /orders/incoming and walk it through Mark Preparing → Mark Completed. After every receiver action it asserts that the requester's /my-orders status pill ("Received", "Preparing", "Completed") updates without a manual refresh — exercising the order_updated socket.io push wired through use-notifications-socket. 2) "receiver cancels after claiming — requester sees Cancelled pill": covers the receiver-side cancel path required by the task by claiming the order and then cancelling from /orders/incoming, and verifies the requester's card flips to the red "Cancelled" pill and loses the timeline step circles. Test design notes - English UI only; the language matrix is already covered for the requester side by order-place-cancel.spec.mjs. - Each test creates a unique requester display name so that incoming cards can be uniquely located by service name + "From <display name>" even if other cards exist on the page. - Status assertions use exact-text matching on the pill to avoid the "Received" / "Awaiting receiver" substring overlap. - afterAll cleans up service_orders, notifications, user_roles and the seeded users (defensive: also deletes any orders referencing those users that weren't explicitly tracked). Other - The new spec is automatically picked up by the existing `pnpm --filter @workspace/tx-os test:e2e` run (testMatch in artifacts/tx-os/playwright.config.mjs already globs *.spec.mjs). - Verified locally: full tx-os e2e suite (9 specs) passes. Replit-Task-Id: bdbee3d1-20e0-4a40-b2b1-51ea16a15bb7 |
||
|
|
a14b589006 |
Audit role permission changes (Task #100)
Record an audit trail every time a role's permissions change and surface recent history inside the role edit dialog. Schema (lib/db): - New `role_permission_audit` table: id, roleId (FK roles), actorUserId (FK users, nullable on delete), previousPermissionIds int[], newPermissionIds int[], createdAt. Created via raw SQL because `drizzle push` currently fails on a pre-existing app_permissions duplicate (followup #148 tracks fixing this). API (artifacts/api-server): - PUT /api/roles/:id/permissions now wraps the permission delete/insert, the legacy audit_logs row, and the new role_permission_audit row in a *single* transaction so the permission set and its audit trail always commit (or roll back) together. Previous IDs are also read inside the transaction to avoid races. - A role_permission_audit row is written on EVERY PUT call (per task spec), including no-op saves; the GET handler computes addedPermissionIds/removedPermissionIds so the History UI can distinguish meaningful changes from no-op saves. - New GET /api/roles/:id/audit (admin-only, ?limit=1..50, default 10) returns recent entries newest-first with computed added/removedPermissionIds and joined actor info. - New tests in tests/role-permission-audit.test.mjs cover write, every-call write (incl. no-op), GET ordering+diff+actor, no-op diff reporting, 404, and limit. Drive-by fixes: - Fixed pre-existing syntax corruption in tests/apps-open.test.mjs (stray `ccaPassa,` line from a prior bad merge that prevented the whole api-server test workflow from running). - Made tests/roles-crud.test.mjs "rejects an invalid name" robust to parallel test execution by scoping the leak check to the bad name instead of relying on a global row count. API spec / codegen (lib/api-spec, lib/api-client-react): - Added `getRolePermissionAudit` operation and `RolePermissionAuditEntry` schema; ran orval codegen. Frontend (artifacts/tx-os): - New RolePermissionHistory component renders inside the role edit dialog (between permissions and Save/Cancel) using useGetRolePermissionAudit. Shows timestamp, actor, added/removed permission names (resolved via permissionsById), and total count. - Save invalidates the audit query so the new entry appears immediately on the next open. - Bilingual i18n strings (en + ar with full Arabic plural variants: zero/one/two/few/many/other) under admin.roles.history*. Verification: - tx-os typecheck passes. - All 5 new audit tests + 13 existing roles tests pass. - E2E (testing skill) verified the full flow: empty state on a fresh role, save adds a permission, reopened dialog shows the new entry with actor name in Arabic. Docs: - replit.md updated to list `role_permission_audit` in Database Tables. Follow-ups proposed: #146 paginate/filter history, #147 audit other admin permission changes, #148 fix drizzle push duplicate. Replit-Task-Id: 9b8886a2-6e76-4072-b345-a772421fa161 |
||
|
|
b5de44485e |
Task #140: 3 attendees-cell refinements in Executive Meetings schedule
(1) Clearing an attendee's name now removes the attendee row instead of
saving an empty record. saveAttendeeName detects empty stripped HTML
(tags + + whitespace) and PUTs the array with the row removed
and sortOrder repacked.
(2) Inline "+ Add attendee" button next to each attendee group, plus
"+ Virtual / + Internal / + External" chips for groups not yet present
in a split cell. Implemented as an optimistic ghost row — clicking +
sets a single global pendingAttendee state and renders a synthetic
EditableCell at the end of the matching group. Only when the user
types a non-empty name and blurs do we PUT the new attendee. Empty
blur or Escape just clears the pending state — no orphan rows reach
the API. The API stays strict (name.min(1)).
(3) Always-visible dashed underline on each attendee EditableCell
makes the multi-attendee wrapped layout discoverable as click targets;
hidden in edit mode (data-[editing=true]) and on print. The group
label ("— Virtual attendance — Webex —") is now pointer-events:none
so clicks pass through to whatever sits underneath instead of being
swallowed by the header text.
Implementation notes:
- EditableCell gained two new props (forceSaveOnBlur, onCancel) so the
ghost row can commit/discard cleanly even when the editor is empty.
- pendingAttendee is a single global state (only one ghost open at a
time across the whole page). All other rows hide their + buttons
while a ghost is open to prevent the user from orphaning typing.
- Locale keys added in ar.json + en.json under
executiveMeetings.schedule.{addAttendee, addVirtualAttendee, …,
newAttendeeAria, editAttendeeAria}.
- Backend executive-meetings.ts unchanged — attendeeSchema still
requires non-empty name.
Verified end-to-end:
- Add attendee, commit on blur, delete by clearing, cancel by Escape
on empty.
- Cross-row gating: opening a ghost in row A hides "+" buttons in
row B until the ghost is committed/cancelled.
- Webex/virtual header click no longer enters edit mode.
Pre-existing TypeScript errors in admin.tsx (codegen drift from #96)
and 3 failing api-server tests are unrelated and out of scope.
Follow-ups proposed: #144 (inline edit attendee titles),
#145 (move attendees between virtual/internal/external groups).
|
||
|
|
1ba05c11d0 |
Task #137: Polish Executive Meetings schedule table (5 refinements + post-review fixes)
Original 5 refinements:
1. Time displayed on a single line (TimeRangeCell with whitespace-nowrap).
2. Time inline-editable with two HH:MM inputs.
3. Reliable attendee inline-edit with placeholder + dir handling.
4. Floating formatting toolbar via portal so it never clips the table.
5. Inline "Add row" tr at the bottom of the table.
Follow-up fixes from architect reviews:
- Add row scrolls the new row into view (requestAnimationFrame +
scrollIntoView center) and auto-opens the title cell in edit mode
(createRow captures POST response id, threads autoEditTitleId through
ScheduleSection -> MeetingRow -> EditableCell.startInEditMode).
autoEditTitleId hardened with a 1.5s fallback clear so it can't go
stale if the title cell never mounts.
- Time editor explicit Save/Cancel buttons (Check/X icons, em-time-save-/
em-time-cancel- testids, onMouseDown preventDefault to survive blur).
- Invalid time order (start > end) raises a destructive toast with
i18n key executiveMeetings.schedule.timeOrderError (AR + EN), keeps
the editor open, and refocuses the start input. Equal start/end is
valid per spec. Server errors also toasted.
- Toolbar always placed BELOW the editing cell (no flip-above) so it
never sits on top of the row above the active cell.
- Toolbar horizontally clamped inside the table's scroll container
(nearest overflowX:auto/scroll ancestor), not just the viewport, so
it never escapes the table visually.
Files touched:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- artifacts/tx-os/src/components/editable-cell.tsx
- artifacts/tx-os/src/locales/{ar,en}.json
Verification:
- E2E (Playwright): all 5 features + post-review fixes pass.
* Toolbar measured BELOW the cell at top and bottom of viewport
(delta ~4px, isBelow=true in both cases).
* Toolbar measured INSIDE the table scroll container's left/right.
* Equal start/end (14:00–14:00) saved without validation toast.
- Manual API: POST 201, PATCH 200, validation 400, DELETE 204.
- Tasks #122 and #125 still functional.
- Pre-existing failing tests (admin.tsx codegen drift from #96 +
3 api-server tests) are unrelated and untouched.
|
||
|
|
0d58a856d6 | Git commit prior to merge | ||
|
|
c0cf2115dd |
Task #96: Show dependency counts in admin app/service/user lists
Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.
Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
(groupCount, restrictionCount, openCount), Service (orderCount),
and UserProfile (noteCount, orderCount, conversationCount,
messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
batch-loads groupCount/restrictionCount/openCount via grouped
COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
loads noteCount/orderCount/conversationCount/messageCount from
notes, service_orders, conversations.created_by, and
messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
delete buttons pre-populate their conflict-state from the row's
count fields so the DeletionWarningDialog shows the warning on
the first click. The lazy 409 fallback still works as a safety
net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.
Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
with 6 tests verifying each list endpoint exposes the new count
fields with the expected non-zero values when dependents exist
AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
(9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
service delete icon ONCE opens the confirmation modal with the
dependency warning ("orders", count >= 1) immediately.
Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
schemas (not list-only sub-schemas) so the same shape is returned
from list and detail endpoints. Code review flagged this as
acceptable but broader than strictly necessary; left as is to
keep the API consistent.
Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
isn't already up — needs a readiness check.
Replit-Task-Id: bd14fe73-9961-431b-ab5a-ab70f116e8c7
|
||
|
|
a759e0eb90 |
Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule: 1. Word-like inline editing of meeting title and attendee names with bold, italic, underline, color, font-family, font-size, and text-align controls (Tiptap-backed EditableCell + FormattingToolbar). 2. Drag-to-reorder column headers (dnd-kit), persisted in the existing em-schedule-cols-v1 storage key. Old up/down buttons removed. 3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder endpoint permutes daily_number + start/end times in a single transaction using a two-phase negative→final assignment that respects the (date, daily_number) UNIQUE constraint and survives partial-day reorders. Audit-logged and role-guarded. 4. Highlight the meeting whose start/end window is "now", refreshed every 60 s. Toggle and color picker live in the customize popover and persist to localStorage (em-current-meeting-highlight-v1). Backend changes - Widened titleAr / titleEn / attendees.name to text. - New artifacts/api-server/src/lib/sanitize.ts with an allowlist for inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees, duplicate, reorder, and applyApprovedRequest (add_attendee) paths so rich text round-trips safely. - Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn; applyApprovedRequest add_attendee sanitizes the inserted name; reorder transaction return value cleaned (`byId` removed). - 5 new tests cover sanitization stripping, reorder happy path, cross-day rejection, permission denial, and applyApprovedRequest sanitization. Pre-existing app_permissions failures are unrelated (tracked by #126/#127). Frontend changes - artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar (custom FontSize Tiptap extension + @tiptap/extension-text-align). - ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation (optimistic with revert on error), highlight tick, and HighlightPrefs. - New SortableHeader component for column drag. - AttendeesCell passes the original attendee index to PUT writes so edits reach the right row even after grouping into virtual/internal/external. - Print page renders sanitized rich text via dangerouslySetInnerHTML on names and titles; attendee.title is rendered as a plain text node (XSS). - Translation keys added in ar.json and en.json. |
||
|
|
5114b207da |
Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule: 1. Word-like inline editing of meeting title and attendee names with bold, italic, underline, color, and font controls (Tiptap-backed EditableCell). 2. Drag-to-reorder column headers (dnd-kit), persisted in the existing em-schedule-cols-v1 storage key. Old up/down buttons removed. 3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder endpoint swaps daily_number in a single transaction using a two-phase negative→final assignment that respects the (date, daily_number) UNIQUE constraint. Audit-logged and role-guarded. 4. Highlight the meeting whose start/end window is "now", refreshed every 60 s. Toggle and color picker live in the customize popover and persist to localStorage. Backend changes - Widened titleAr / titleEn / attendees.name to text (applied via direct ALTER TABLE because db:push --force is currently blocked by Task #126). - New artifacts/api-server/src/lib/sanitize.ts with an allowlist for inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees, duplicate, and reorder paths so rich text round-trips safely. - Code-review fix: duplicate handler now re-sanitizes titleAr/titleEn. - Code-review fix: print page no longer interpolates the unsanitized attendee.title into dangerouslySetInnerHTML. - 4 new tests cover sanitization stripping, reorder happy path, cross-day rejection, and permission denial. Pre-existing app_permissions failures are unrelated and tracked by Task #126. Frontend changes - artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar. - ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation (optimistic with revert on error), highlight tick, and HighlightPrefs. - New SortableHeader component for column drag. - AttendeesCell now passes the original attendee index to PUT writes so edits reach the right row even after grouping into virtual/internal/ external sections. - Print page renders sanitized HTML via dangerouslySetInnerHTML on names and titles (titles still rendered as plain text on the print page). - Translation keys added in ar.json and en.json. Drift - Sanitization for attendee.title was identified by the architect review as a defense-in-depth gap and proposed as Task #130 rather than fixing inline; the print-page XSS path that depended on it was fixed directly. |
||
|
|
be4ecb30bb |
Task #95: Let admins export the audit log as CSV
What was done - Added a new admin-only endpoint GET /api/admin/audit-logs/export that streams the filtered audit log as a CSV download. Honors the same `action`, `from`, and `to` query parameters as the list endpoint, validated through a shared parseFilters/buildWhere helper extracted from the existing handler. - Columns: action, actor_username, target_type, target_id, created_at, metadata (compact JSON). Rows ordered newest-first and capped at 50,000 to bound response size. UTF-8 BOM prepended so spreadsheet apps (incl. Excel) detect encoding correctly for Arabic content stored in metadata. - Response sets Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="audit-log-YYYY-MM-DD.csv", Cache-Control: no-store, and is written via res.write streaming. - Updated lib/api-spec/openapi.yaml with operationId exportAuditLogsCsv and ran codegen to regenerate the React client + Zod schemas. - Frontend (artifacts/tx-os/src/pages/admin.tsx → AuditLogPanel): added an "Export CSV" button next to the existing filter actions. Clicking it fetches the export URL with current filters using same-origin cookies, triggers a browser download, and surfaces a localized error if the request fails. The button is disabled while filters are invalid or while a download is in flight, and a spinner replaces the icon during export. - Added bilingual translation keys admin.audit.export.button / admin.audit.export.failed in en.json and ar.json. Security hardening (post code-review) - csvEscape now prefixes a single quote when a cell value begins with one of =, +, -, @, tab, or CR, mitigating CSV/spreadsheet formula injection (Excel, LibreOffice, Google Sheets evaluate such cells as formulas). Verified end-to-end by inserting a synthetic audit row whose action started with "=" and confirming the export wrote it as "'=...". Verification - Typecheck of tx-os passes; api-server has only pre-existing executive-meetings errors unrelated to this change. - Manual curl smoke test: 200 with correct headers, BOM present, action and date filters honored, invalid date returns 400. - E2e UI test (Playwright via testing skill): logged in as admin, opened the Audit Log section, downloaded the unfiltered CSV (correct filename and header row), then re-exported with action=app.delete and confirmed only matching rows came back. - Targeted security check confirmed CSV injection mitigation (see above). Notes / drift - None for the spec. The generated `exportAuditLogsCsv()` helper in lib/api-client-react is typed Promise<Blob> but, because customFetch auto-infers text/csv as text, would currently return text if anyone called it directly. The Audit Log UI uses a raw fetch with credentials: "include" to download the blob, so this does not affect the feature. Updating the generated client's responseType handling is project-wide config and is intentionally out of scope here. Follow-ups proposed - #123 Add automated tests for the audit log CSV export (test_gaps) - #124 Let admins pick which columns to include when exporting the audit log (next_steps) Replit-Task-Id: 88a50100-caa7-4b37-b9da-cfdc42bee119 |
||
|
|
5b35261785 |
Task #119 — Make the meetings schedule table fit phones and tablets
Problem:
The Executive Meetings schedule table used `tableLayout: "fixed"` with
hard-coded column widths totalling ~1100px (number 56 + meeting 320 +
attendees 600 + time 120). On any phone or iPad portrait this forced a
horizontal scrollbar and clipped attendee text.
Approach (artifacts/tx-os/src/pages/executive-meetings.tsx):
1. Added a small useMediaQuery hook (matchMedia listener with cleanup,
guarded for SSR).
2. ScheduleSection now renders TWO views with CSS-based switching so
nothing depends on JS state for visibility:
- Mobile cards container: className "md:hidden print:hidden",
data-testid="em-schedule-cards". Stacked cards with number badge,
title/location, time, and attendees. Same row-color background.
- Tablet/desktop table container: "hidden md:block print:block".
Always shown for print regardless of viewport.
3. Within the table:
- className now "table-auto xl:table-fixed print:table-fixed".
Tailwind classes alone control whether widths are enforced.
- <colgroup> + per-cell widths are emitted unconditionally so the
same widths apply at xl screens AND in print at any viewport. In
`table-auto` mode (md..<xl screens), browsers treat them as
hints and shrink columns to fit, so no horizontal scroll.
- Resize handles are screen-only (`isXl &&`); they need viewport-
driven layout to work and would just block touch scrolling.
4. MeetingRow now always provides width + overflow:hidden inline.
With break-words on the inner divs, wrapping behavior remains
correct under both layout modes.
5. New MeetingCard component for the mobile layout: number badge,
title/location, time, attendees in a stacked layout, with the
row-color background and a touch-friendly always-visible color
picker (RowColorPickerInline). The Columns customizer still gates
which fields appear via visibleColumns.some(...).
6. Refactored RowColorPicker into a shared RowColorPickerSwatches
body plus two trigger variants:
- RowColorPicker: original hover-only absolute trigger for the
table number cell (testid em-row-color-trigger).
- RowColorPickerInline: always-visible trigger for cards (testid
em-row-color-trigger-inline).
Verification (testing skill):
- Screen layout at 1280, 1440, 768, 390 px: PASS
- Desktop: table visible, em-schedule-cards hidden.
- Tablet 768: table visible, NO horizontal scroll on body.
- Phone 390: cards visible, table hidden, NO horizontal scroll.
- Print fidelity at sub-xl viewports (768 and 390): PASS
- getComputedStyle(table).tableLayout = "fixed" (print:table-fixed
takes effect at any viewport).
- First column header width ≈ 56px (configured "number" width)
confirms colgroup widths are honored in print at narrow widths.
- Print-only header visible, table visible (print:block),
em-schedule-cards hidden (print:hidden).
- First architect review caught a print-fidelity regression
(previous draft gated colgroup on isXl); fixed by removing all
isXl gating from the widths and letting CSS classes alone govern
layout mode. Re-tested and verified.
Out of scope (per task brief):
- The dedicated print artifact (executive-meetings-print.tsx) —
owned by Task #120.
- Data model, API, columns, theme/colors.
|
||
|
|
abe6f6ab1d |
Task #119 — Make the meetings schedule table fit phones and tablets
Problem:
The Executive Meetings schedule table used `tableLayout: "fixed"` with
hard-coded column widths totalling ~1100px (number 56 + meeting 320 +
attendees 600 + time 120). On any phone or iPad portrait this forced a
horizontal scrollbar and clipped attendee text.
Approach (artifacts/tx-os/src/pages/executive-meetings.tsx):
1. Added a small useMediaQuery hook (matchMedia listener with cleanup,
guarded for SSR).
2. ScheduleSection now renders TWO views with CSS-based switching so
nothing depends on JS state for visibility:
- Mobile cards container: className "md:hidden print:hidden",
data-testid="em-schedule-cards". Stacked cards with number badge,
title/location, time, and attendees. Same row-color background.
- Tablet/desktop table container: "hidden md:block print:block".
Always shown for print regardless of viewport width.
3. Within the table:
- className now "table-auto xl:table-fixed print:table-fixed".
- <colgroup> with pixel widths only emitted when isXl (or SSR).
- <th width style> only applied when isXl.
- Resize handles only rendered when isXl (touch users at <xl get
no col-resize handles, so they don't fight scrolling).
4. MeetingRow gained an `applyFixedWidths` prop. When false, it drops
per-cell width and overflow:hidden so cells wrap freely. break-words
was already present on title/location.
5. New MeetingCard component for mobile: number badge, title +
location, time, attendees in a stacked layout, with row color
background and a touch-friendly always-visible color picker
(RowColorPickerInline). The Columns customizer still gates which
fields appear in the cards via visibleColumns.some(...).
6. Refactored RowColorPicker into a shared RowColorPickerSwatches body
plus two trigger variants:
- RowColorPicker: original hover-only absolute trigger for the
table number cell (testid em-row-color-trigger).
- RowColorPickerInline: always-visible trigger for cards (testid
em-row-color-trigger-inline).
Verification:
- Tested with the testing skill at 1280, 1440, 768, and 390 px.
- Desktop (1280/1440): table visible, em-schedule-cards hidden.
- Tablet (768): table visible, no horizontal scroll on body.
- Phone (390): cards visible, table hidden, no horizontal scroll
(body.scrollWidth = 390).
- Architect review: Pass. RTL preserved (cards and table both pass
dir), Columns customizer gates both views, all existing testids
intact (em-row-*, em-col-header-*, em-row-color-*, em-customize-*,
em-schedule-heading), hooks used unconditionally.
Out of scope (per task brief):
- The dedicated print artifact (executive-meetings-print.tsx) — owned
by Task #120.
- Data model, API, columns, theme/colors.
- Architect noted a non-blocking nuance: client-side Cmd+P from a
<xl viewport will keep table-auto column widths because isXl is
false at print time. This is a print-fidelity concern, not a
responsiveness blocker, and the dedicated print artifact (Task
#120) is the proper home for printing concerns.
|
||
|
|
9a3cf120ce |
Show readable summaries instead of raw JSON in the audit log
Task #94: Each admin audit log row now renders a localized one-line summary derived from the entry's action + metadata, e.g. "Deleted group 'audit-test-group'" or "تم حذف المجموعة 'X'", instead of just showing the raw target type/id. Implementation - Added `formatAuditSummary(entry, t, lang)` in `artifacts/tx-os/src/pages/admin.tsx` covering all known audit actions emitted by the API (group/user/role/app/service/settings/ auth create/update/delete/permission/membership variants, including forced deletions). Returns null for unknown actions so the row gracefully falls back to the existing "Target: type #id" display. - The summary now occupies the prominent text slot in `AuditLogRow`. The raw action code stays in the small badge (it is also the value the action-filter dropdown uses), and the timestamp moved next to it. The expand toggle still reveals the full JSON metadata for power users (data-testid `audit-metadata-<id>` unchanged). - Added `admin.audit.summary.*` keys plus a pluralized `admin.audit.unit.*` helper map in en.json and ar.json. The Arabic unit keys provide all six CLDR plural forms (zero/one/two/few/ many/other) so counts read naturally; English uses one/other. Verification - `tsc --build` clean for the tx-os artifact. - e2e test (admin login → create + delete a uniquely named group → open Audit Log → confirm Arabic and English summaries render, raw JSON is still available behind the toggle, and the action pill still shows the raw action code) passed. Follow-up proposed - #117 "Show readable action names in the audit log filter dropdown" (the filter dropdown still lists raw action codes). Replit-Task-Id: b47aacde-087e-4a2b-8fb2-0e63cb1936e4 |
||
|
|
bdc19d5012 |
Prevent duplicate app permission entries from being saved
Original task (#91): The `app_permissions` join table had no unique constraint, allowing the same `(app_id, permission_id)` pair to be inserted repeatedly. The Admin Panel restriction had grown to 24 duplicate rows. This change prevents that recurring at the DB level and verifies that the existing conflict-safe insert path keeps working. Schema change: - `lib/db/src/schema/apps.ts`: added a composite primary key on `(app_id, permission_id)` for `appPermissionsTable`, matching the pattern used by other join tables in this repo (rolePermissions, userRoles, groupApps, etc.). This is enforced at the database level. DB migration: - Cleaned up the 23 duplicate rows still present in the DB before applying the constraint (kept the earliest row per pair using `ctid`), then ran `pnpm --filter @workspace/db run push` to sync the schema. Verified the new primary key `app_permissions_app_id_permission_id_pk` rejects duplicate inserts. Graceful handling of duplicates: - The seed script (`scripts/src/seed.ts`) already uses `.onConflictDoNothing()` when inserting into `app_permissions`. With the new primary key, that call is now properly idempotent (no longer silently allowing duplicates). - There is currently no HTTP route or admin UI that POSTs into `app_permissions` (the task description listed `routes/apps.ts` as a relevant file, but no such handler exists today). The constraint itself is what prevents future regressions, and any future endpoint should follow the seed's `.onConflictDoNothing()` pattern. Tests: - Added `artifacts/api-server/tests/app-permissions-unique.test.mjs` with two cases that prove the new behavior: 1. A plain duplicate INSERT fails with SQLSTATE 23505 (unique violation) and only one row remains. 2. `INSERT ... ON CONFLICT DO NOTHING` (the pattern Drizzle's `.onConflictDoNothing()` emits) handles duplicates gracefully: no error thrown, `rowCount` is 0, and exactly one row remains. - All previously-passing api-server tests for apps/groups still pass after the schema change. Replit-Task-Id: 0589a4dc-5898-4c66-8feb-3cd48289fe89 |
||
|
|
371c44baba |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight
only. Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full
rewrite:
- ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained
requireMutate / requireApprove / requireRequest / requireAdminAudit on the
appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a
single shared upsertFontSettingsHandler (no Express method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- GET /notifications now accepts ?date=YYYY-MM-DD and joins through the
meetings table to scope notifications to the selected day; returns []
immediately when no meetings exist on that date.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin +
executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized
users never see Manage / Approvals / Audit / Tasks tabs (matrix in the
in-file isSectionVisible helper).
- Approvals: review() sends {status, reviewNotes}; new "Send back for edits"
(needs_edit) button beside Approve / Reject.
- Requests default scope is "mine" for non-approvers and "all" for approvers
(no over-exposure to plain requesters).
- Notifications visibility uses canRead AND the query is scoped by the same
selected `date` as the schedule (queryKey + ?date=… included).
- PdfSection: primary "Print + Archive" button (testid em-print) does archive
then print in one click; em-print-only and em-archive remain as separate
buttons.
- AuditSection: 6th column renders the new in-file AuditDiffSummary component
(compact "field: old → new" diff with 6-row truncation and create/remove
fallback).
- New tWithFallback(t, key, fallback) helper used in audit + notifications
cells (avoids strict TFunction overload errors); apiJson rewritten to
extract body/headers separately and stringify rawBody, eliminating the
"Record<string, unknown> not assignable to BodyInit" TS errors. tsc
--noEmit on executive-meetings.tsx is now clean.
- Print page (executive-meetings-print.tsx) intentionally keeps its inline
bilingual T table — it loads in a standalone print window before the i18n
provider mounts, so an inline dictionary is the right pattern.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate,
replace_attendees, done},
.pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201, nested
POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data, /notifications?date=YYYY-MM-DD filters
correctly.
- tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is
clean (pre-existing admin.tsx errors are unrelated to this task).
- Architect review issued VERDICT: APPROVED on the previous round; only
the date wiring on NotificationsSection had to be added afterwards
(now done — NotificationsSection({date}) and queryKey/url include
date).
Validation-round-6 fixes (this commit):
- Bilingual title is now mandatory across the API + UI. Backend Zod
schema (meetingBaseFields.titleEn) requires .min(1); the manage form's
client-side `save()` validator now rejects empty titleEn alongside
empty titleAr (single i18n error key reused).
- Tasks: full reassign + edit flow. Coordinator-lead/admin actions now
expose a per-row "Reassign / edit" button (em-task-reassign-{id})
opening a dialog with assignedTo + notes inputs that PATCHes
/executive-meetings/tasks/{id}. New i18n keys
executiveMeetings.tasks.{reassign,reassigned} (AR + EN).
- Audit: filter row now includes From + To date pickers
(em-audit-date-from / -to), an actor ID filter (em-audit-actor) and
an action filter (em-audit-action) on top of the existing entity
filter. Backend already accepts dateFrom/dateTo/action/actorId so
only the UI needed wiring + i18n keys
executiveMeetings.audit.filter.{from,to,actorId} (AR + EN).
- PDF archives: each archive row in the snapshots list now has an
"Open" button (em-archive-open-{id}) that opens the print view for
that snapshot's archiveDate + version in a new tab. New i18n key
executiveMeetings.pdf.openArchive (AR + EN).
- Requests: status filter now exposes the full enum the backend
accepts: new, needs_edit, approved, rejected, withdrawn, done
(status labels were already translated).
- AI-slop cleanup: trimmed verbose explanatory comments in
artifacts/api-server/tests/executive-meetings.test.mjs and the
artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs.
- Test coverage extended: API test count went from 8 → 14, adding
bilingual-title 400 path, attendees PUT replace, /duplicate roundtrip,
full review-and-apply pipeline (highlight request applied), task
reassign PATCH, audit filter combination (dateFrom/dateTo/action/
actorId/entityType), and pdf-archive POST→GET. All 14 pass against
the live API. The Manage create E2E (1 spec) still passes.
Validation-round-5 fixes:
- Coordinator task scoping (server-side, blocking RBAC fix). Added
TASK_BROAD_VIEW_ROLES (admin + office_manager + coord_lead). Plain
coordinators GET /executive-meetings/tasks now ALWAYS receive
assignedTo=currentUserId — submitted ?mine=0 / ?assigneeId=other are
ignored. /me also exposes a new canViewAllTasks flag for the UI.
- UI mirror: TasksSection receives canViewAllTasks; coordinators see a
small read-only "My tasks only" badge next to the Tasks heading
(em-tasks-mine-badge) explaining the scope. New i18n keys
executiveMeetings.tasks.{myTasksOnly,myTasksOnlyHelp} (AR + EN).
- Seed: scripts/src/seed.ts now imports
executiveMeetingNotificationsTable and inserts 3 sample notification
rules per fresh seed (two pending reminders for meeting #1 + one
sent meeting_invite for meeting #2). Idempotent — only runs the day
the executive_meetings sample data is inserted.
- Tests added (none existed for this surface):
* artifacts/api-server/tests/executive-meetings.test.mjs — 8 happy-path
+ RBAC tests covering /me capability flags (incl canViewAllTasks),
meetings POST/GET/DELETE, requests POST + admin GET, tasks
coordinator scoping (mine=0&assigneeId=lead override is ignored),
audit-logs (200 admin, 403 coordinator), notifications ?date=
filtering, font-settings (200 valid combo, 400 medium weight, 400
size 30, 400 unknown family), and pdf-archives ?date=. All 8 pass.
* artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs —
Playwright UI test that logs in as admin, opens the Manage tab via
em-nav-manage, fills the create dialog (titleAr + titleEn + date),
saves, asserts POST /api/executive-meetings returns 201, and
verifies the row landed in the DB with the expected title and date.
Validation-round-4 fixes:
- Manage attendee editor: added per-row title input alongside name +
deterministic ChevronUp/ChevronDown reorder controls (sort order is
recomputed on save). New i18n keys
executiveMeetings.manage.attendees.{moveUp,moveDown}.
- Manage tab: per-row Copy button opens a new "Duplicate to date" dialog
that calls POST /executive-meetings/:id/duplicate with a target date
picker; on success it switches the day view to the chosen date. New
i18n keys executiveMeetings.manage.{duplicate, duplicateToDate,
duplicated, duplicateFailed}.
- Print page (executive-meetings-print.tsx) now uses react-i18next with
new locale block executiveMeetings.print.* (AR + EN parity), forces
i18n.changeLanguage(lang) so ?lang=en always prints English. Attendees
cell in the print table is now textAlign: center (matches the Step 1
schedule).
- Font settings tightened to spec: families = system/Cairo/Tajawal/
Noto Naskh Arabic/Amiri (dropped Inter, monospace), weights =
regular/bold (dropped medium, semibold), alignment = start/center
(dropped end), size range = 12–22 (was 10–32). Enforced server-side
in fontSettingsSchema (Zod) and mirrored in the UI selects/range.
Verified: PATCH font-settings returns 200 for valid combo, 400 for
fontWeight="medium", 400 for fontSize=30.
Validation-round-3 fixes:
- Tasks RBAC tightened end-to-end: new TASK_VIEW_ROLES = admin +
office_manager + coord_lead + coordinator. GET /tasks and
PATCH /tasks/:id now require requireTaskView. /me exposes new
canViewTasks flag. Frontend isSectionVisible("tasks") gated on
canViewTasks (so CEO/viewer never sees the Tasks tab or hits the
endpoint). Verified: admin /me returns canViewTasks=true.
- GET /requests gained dateFrom + dateTo (createdAt range) and a
requester filter that is honored only for approver/audit roles
(non-approvers stay locked to their own requests). Verified curl
with ?dateFrom=...&dateTo=... returns 200.
- Task dueAt schema accepts BOTH YYYY-MM-DD (from <input type="date">)
and full ISO datetime; date-only is normalized to start-of-day UTC,
empty string clears the field. Verified curl POST /tasks with
{"dueAt":"2026-12-31"} returns 201 with stored
dueAt = 2026-12-31T00:00:00.000Z.
Validation-round-2 fixes:
- GET /executive-meetings/requests: added server-side least-privilege so
only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every
other executive role is force-scoped to requestedBy = self regardless
of the client's `mine` flag (closes the RBAC overexposure flag).
- isSectionVisible("tasks") now also returns true for canSubmitRequest,
so executive_coordinator sees the Tasks tab (coordinators execute the
follow-ups generated from approved requests).
- Requests "new request" dropdown now exposes ALL twelve backend request
types: create, edit, delete, reschedule, add_attendee, remove_attendee,
change_location, cancel_meeting, note, highlight, unhighlight, other.
- i18n parity: added en.json + ar.json keys for the five missing request
types (add_attendee, remove_attendee, change_location, cancel_meeting,
note) and the two missing statuses (needs_edit, done) under
executiveMeetings.requests.{type,status}.
- Fixed wrong key reference: reschedule form's date row was using
manage.field.date (does not exist) — switched to manage.field.meetingDate.
Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112):
- Real notifications delivery, server-side PDF rendering, attendee
reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync,
expanded seed data including executive_meeting_notifications sample
rows.
|
||
|
|
0b7b571a1e |
Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
attendees column widest, tighter padding, navy/white/gray + red highlight only.
- Header label "م" → "#" (ar.json).
Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
requests work without an existing meeting. db push applied.
Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full rewrite:
- All routes (including /me) gated by requireExecutiveAccess. Fine-grained
middleware: requireMutate / requireApprove / requireRequest /
requireAdminAudit on the appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
{status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
{action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired directly
to the same shared upsertFontSettingsHandler (no method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- Removed all `as never` casts via $inferInsert/Partial typing.
Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES
(admin + executive_*).
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Notifications visibility now uses canRead (was canViewAudit).
- Approvals: review() sends {status, reviewNotes} and a new
"Send back for edits" (needs_edit) button is rendered alongside
Approve/Reject.
- PdfSection: primary "Print + Archive" button (testid em-print) does
archive then print in one click; em-print-only and em-archive remain.
- AuditSection: 6th column renders the new in-file AuditDiffSummary
component (compact "field: old → new" diff with truncation).
- Print page (executive-meetings-print.tsx): kept its inline bilingual T
table — it loads in a standalone print window before the i18n provider
mounts, so an inline dictionary is the right pattern. No hardcoded UI
text remains in the main page.
i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
.audit.col.changes, .audit.{created, removed, moreChanges},
.audit.entity.pdf_archive,
.audit.action.{approved, rejected, needs_edit, withdraw, apply,
duplicate, replace_attendees, done},
.pdf.{print = "Print + archive"/"طباعة وأرشفة", printOnly,
archivedAndPrinted}.
Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201,
nested POST /:id/requests 201, PATCH /requests/:id status approval 200,
audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
200 with persisted data.
- Architect re-reviewed twice: VERDICT: APPROVED on the second pass
after fixing /me gating and the PATCH font-settings dispatch.
Out of scope (handled in follow-ups #110/#111/#112): real notifications
delivery, server-side PDF rendering, mobile polish.
Drift: none material. Followed plan T1–T8.
|
||
|
|
bf85d7066e |
Task #108 — Executive Meetings Phase 2
Schedule UI polish, full CRUD, Requests/Approvals/Tasks/Audit/PDF/Print/
Font Settings/Notifications-placeholder, audit logging on every mutation,
fine-grained RBAC, bilingual i18n.
Backend (artifacts/api-server/src/routes/executive-meetings.ts)
- All mutations transactional + audit-logged.
- Approve/reject/needs_edit use atomic UPDATE WHERE status='new' RETURNING;
race losers return 409 RACE_ALREADY_REVIEWED.
- applyApprovedRequest() applies reschedule / change_location / add_attendee /
remove_attendee / highlight / unhighlight / cancel_meeting / create / edit /
delete in-tx; effective-time validation throws APPLY_VALIDATION → 400.
- Approve auto-creates linked follow_up_<reqType> task with requestId set.
- Marking a task with requestId as 'completed' propagates request → 'done'
(in same tx, audit-logged).
- New endpoints: PUT /:id/attendees, POST /:id/duplicate,
GET/POST /pdf-archives.
Schema (lib/db/src/schema/executive-meetings.ts)
- executive_meeting_requests.meetingId nullable so 'create' requests work.
- pdf_archives table.
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx + new
executive-meetings-print.tsx, App.tsx route)
- Schedule polish: # column, RTL order, attendees widest, navy/white/gray,
red highlight only.
- Tabs: Manage / Requests / Approvals / Tasks / Audit / PDF / Notifications /
Font Settings.
- Requests form now sends STRUCTURED fields (meetingDate, startTime, endTime
for reschedule; location/url for change_location; name/title for
add_attendee; etc.) — previously sent only {note}, which prevented apply
from doing anything.
- Approve handler invalidates ALL ['/api/executive-meetings', *] keys + tasks.
- PDF tab lists archives; Archive snapshot button; Print opens new route.
i18n (en.json/ar.json)
- Full executiveMeetings.* key set including pdf.archive*, attendees.title/id.
Verified
- E2e regression: create meeting → reschedule request (structured) → approve
→ meeting moves to new date → task created → mark task complete → request
becomes 'done'. Negative case (start>end) → approve rejected.
- Architect final review: PASS, no severe blockers.
Out of scope per spec: real notification delivery, real PDF rendering, iCal,
mobile polish.
|
||
|
|
b4d47e1012 |
Executive Meetings Phase 2: full module + atomic audit
Original task (#108): polish the daily-schedule UI and ship the remaining 8 sections of the Executive Meetings module (Manage, Requests, Approvals, Tasks, Notifications, Audit, PDF, Font Settings) with bilingual i18n and fine-grained RBAC. What changed: - Schedule UI: centered cells, '#' header (not 'م'), attendees widest, RTL column order locked, navy/white/gray + red highlighting only. - Schema: made executive_meeting_requests.meetingId nullable so 'create-meeting' suggestions don't need a target row. - Backend rewrite of artifacts/api-server/src/routes/executive-meetings.ts: GET /me (now returns userId), full CRUD for meetings (transactional attendee replace), requests CRUD + approve/reject/withdraw, tasks CRUD with assignee status updates, audit-logs GET (admin), notifications GET, font-settings GET/PATCH (per-user + global). RBAC via 5 role sets and a makeRequireRoles middleware factory. - Audit-in-transaction: every mutation (meeting/request/task/font CRUD) wraps the DB write AND the audit-log insert in the same db.transaction so audit entries cannot drift from state if either insert fails. This was the main finding from the architect review and is now fixed. - Path-to-regexp 8 fix: replaced inline ':id(\\d+)' with router.param('id') + next('route') guard, plus NaN guards in handlers. - Frontend rewrite of artifacts/tx-os/src/pages/executive-meetings.tsx (~2200 lines): 8 new section components, RBAC-aware UI via /me, per-task assignee check now shown for status buttons. - i18n: full executiveMeetings.* key set added in ar.json + en.json. Verified: full e2e (admin login -> all 9 tabs -> create meeting -> submit request -> approve -> audit shows full chain -> font save) plus a smoke regression after the audit-in-tx refactor (atomic audit row created in lockstep with mutation). Out of scope (proposed as follow-ups #110-#112): real notification delivery, real PDF generation (currently window.print), and automated integration tests for the new endpoints. The pre-existing 'apps-open.test.mjs' syntax error in the test workflow is unrelated to this task. |
||
|
|
f7b8093d04 |
Add Reorder shortcut to finished orders on /my-orders
## Original task (Task #66) Let users quickly re-order a service from their order history. On every order card whose status is `completed` or `cancelled`, show a "Reorder" button (Arabic: "اطلب مجددًا") that opens the existing OrderServiceModal pre-filled with the original notes. Submitting creates a brand-new order via the existing endpoint; the original order stays unchanged. ## Implementation - `artifacts/tx-os/src/components/order-service-modal.tsx`: added an optional `initialNotes` prop and seeded the notes textarea with it (truncated to NOTES_MAX) whenever the modal opens. Existing services page callers are unaffected (prop defaults to undefined => empty notes). - `artifacts/tx-os/src/pages/my-orders.tsx`: - Imported the `RotateCcw` icon and `OrderServiceModal`. - Added a "Reorder" button (with icon) on `OrderCard` that shows only for finished orders, alongside the existing cancel/delete actions. - Hoisted reorder modal state to `MyOrdersPage` via a new `reorderTarget` state and an `onReorder` callback wired through `OrderCard`. - Render `OrderServiceModal` at the page level using the original order's service info and notes; closing/submitting clears state. - `artifacts/tx-os/src/locales/{en,ar}.json`: added `myOrders.reorder` ("Reorder" / "اطلب مجددًا"). ## Notes / non-deviations - Task description references `artifacts/teaboy-os/...`; the actual artifact in this repo is `artifacts/tx-os/`. Same files, same intent. - Submitting reuses `useCreateServiceOrder` so the original order is untouched and standard error/success toasts fire from the modal. ## Verification - `pnpm exec tsc --noEmit` shows no new errors in the touched files. - E2E test: created a fresh user with one completed and one cancelled order, logged in, opened /my-orders, confirmed the Reorder button appears with notes prefilled in the modal, edited the notes, submitted, and verified a new pending order with the edited notes appeared while the original order remained unchanged. Replit-Task-Id: 2140b360-9d2f-45d9-9ddc-267ea29f7d7c |
||
|
|
6c160530fa |
Add e2e tests for the order placement and cancellation flow
Original task (#65): Persist coverage for the new "Order" + "My Orders" flow with a Playwright spec under artifacts/teaboy-os/tests/ that places an order, sees the "Awaiting receiver" pill in /my-orders, cancels it, and verifies the red cancelled pill replaces the timeline and the cancel button disappears. Both Arabic and English label paths must be exercised, and the spec must run under `pnpm --filter @workspace/teaboy-os test:e2e`. Implementation - Added artifacts/tx-os/tests/order-place-cancel.spec.mjs (the artifact is now `tx-os`; `teaboy-os` no longer exists, so the spec lives in the same tests directory as `leave-group-successor.spec.mjs` and is picked up automatically by the existing test:e2e Playwright config). - Style mirrors leave-group-successor.spec.mjs: file-scoped DB pool, per-test seeded user with the `user` role, after-all cleanup that deletes the spec's orders, the user's stray orders/notifications, user_roles, and the user itself. - The spec runs the same flow twice, once with English labels and once with Arabic labels, driven through a `placeAndCancelFlow(page, lang, labels)` helper that: 1. Forces the UI language via `page.addInitScript` writing the `tx-lang` localStorage key BEFORE any page script runs. 2. Seeds the test user with `preferred_language` matching `lang`, because AuthContext + login.tsx call `i18n.changeLanguage(user.preferredLanguage)` on login and would otherwise override the localStorage seed. 3. Logs in via the real /login form, navigates to /services, clicks the available service tile (matched by aria-label = localized name), submits the order modal, and waits for the POST /api/orders 201 to capture the order id. 4. Navigates to /my-orders, asserts the first card contains the service name, the pending pill ("Awaiting receiver" / "بانتظار المستلِم"), and 4 timeline step circles. 5. Clicks the in-card Cancel button, confirms via the alertdialog, waits for the PATCH /api/orders/:id/status 200 → cancelled. 6. Asserts the timeline step circles are gone, the centered red cancelled pill ("Cancelled" / "ملغى") is present, and the Cancel button is removed from the card. Verification - `pnpm --filter @workspace/tx-os exec playwright install chromium` - `cd artifacts/tx-os && pnpm exec playwright test --config playwright.config.mjs` → all 6 specs pass (the 4 pre-existing ones + the 2 new EN/AR tests). Deviations - Task referenced the `teaboy-os` artifact path; the actual artifact in this codebase is `tx-os`, so the spec was placed under `artifacts/tx-os/tests/` and registered with the existing `pnpm --filter @workspace/tx-os test:e2e` script (the original `teaboy-os` filter no longer exists). Replit-Task-Id: d2139d0e-9ee0-4795-8f94-29e1cfe4b35a |
||
|
|
aa6866936d |
Task #89: Permissions picker for roles in admin dashboard
Original task: let admins assign permissions to roles from the dashboard.
Changes:
- lib/api-spec/openapi.yaml: added Permission and ReplaceRolePermissionsBody
schemas; added GET /permissions, expanded GET /roles/{id}/permissions to
return full Permission objects, and added admin-guarded
PUT /roles/{id}/permissions. Ran codegen to refresh generated hooks/zod.
- artifacts/api-server/src/routes/roles.ts:
* Added a single isSystemRole helper and PROTECTED_ROLE_NAMES set.
* Added serializePermission + GET /permissions.
* Expanded GET /roles/:id/permissions (404 on missing role, full
Permission objects).
* New PUT /roles/:id/permissions: blocks system/protected roles
(400 with code "system_role_permissions"), rejects non-positive /
non-integer permissionIds with 400, validates all ids exist (404),
deletes+inserts atomically in a transaction, and notifies role
holders via emitAppsChangedToRoleHolders.
* Hardened legacy POST /roles/:id/permissions and
DELETE /roles/:id/permissions/:permissionId so they also reject
system-role mutations with the same code, closing the bypass that
the new endpoint was meant to prevent.
* Re-used isSystemRole in PATCH/DELETE role flows.
- artifacts/tx-os/src/pages/admin.tsx: added a permissions checkbox list
inside the role editor dialog, disabled (read-only) for system /
protected roles with a hint. Save flow runs updateRole then
replaceRolePermissions only when the set actually changed and the role
is non-system. Front-end ROLES_PROTECTED_NAMES set mirrors the
back-end fallback so admin/user/order_receiver are recognised even
when the legacy DB column is 0 (also gates the role-card system badge
and delete button).
- artifacts/tx-os/src/locales/en.json + ar.json: new translation keys
for the permissions picker, system-role read-only hint, empty list,
and error toasts.
Verification: tx-os and api-server typecheck clean. End-to-end test
passes: admin login → edit a created role → toggle/save permissions →
re-open and confirm round-trip → admin role dialog shows disabled
checkboxes and read-only hint → PUT on admin role returns 400
"system_role_permissions". curl regression checks: legacy POST/DELETE
on admin role return the same 400/code; PUT rejects float / negative /
string ids with 400.
Replit-Task-Id: 74180397-afdf-4489-976a-a6fe099684bd
|