main
317 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b228b7d652 |
Task #349: Executive Meetings PDF improvements
- Schema: add `font_color` (hex, default #000000) and `logo_object_path` to `executive_meeting_font_settings`. Pushed via drizzle-kit. - PDF renderer: - Add `fontColor` to PdfFontPrefs (body cells only; header chrome and logo intentionally ignore it to preserve branding). - Add `rowColor` to PdfMeeting and a ROW_COLOR_FILL palette kept in lockstep with the on-screen swatches; deliberately stop painting the legacy `isHighlighted` overlay so the archived PDF reflects editorial state instead of the viewer's transient cursor. - Add optional `logo: Buffer`. Header now reserves a left-anchored logo box and centers the title in the remaining width; bad image bytes log + fall back to the no-logo layout instead of crashing. - API route: - Extend fontSettingsSchema with strict #RRGGBB regex and /^/objects/<id>$/ regex for logoObjectPath. - resolveFontPrefsForUser now returns { font, logoObjectPath }. - loadLogoBytes downloads the brand asset via ObjectStorageService. - Logo only writable on the global-scope row. - PDF labels switched to "قائمة بأسماء حضور الاجتماعات" / "Meeting Attendance List" per the user's printed sample. - Frontend (executive-meetings.tsx): - FontPrefs gains fontColor; FontSettingsResponse.global gains logoObjectPath; DEFAULT_FONT and effectiveFont updated. - buildFontStyle applies fontColor to on-screen rows. - FontSettingsSection: native color picker + hex text input; logo upload (PNG/JPEG) via @workspace/object-storage-web's useUpload, visible only at the global scope for admins. - Locales: AR/EN keys for fontColor + logo.{label,upload,replace, remove,uploading,uploadFailed,globalOnly}. - Tests: existing font-settings roundtrip extended with fontColor; new test rejects malformed fontColor and non-/objects logo paths. Type-check clean for api-server and tx-os; new font-settings tests pass. Other test failures in the suite pre-date this change. |
||
|
|
4a72805296 |
Task #312: auto-sort executive-meetings schedule by start_time
User report (AR): "اريدها ترتيبها تلقائيا" — after a non-drag write
(typing 13:00 into a row sitting above a 12:00 row, creating a meeting
with an explicit start time, etc.) the schedule's daily_number stayed
frozen at the row's prior position, so the visible list was no longer
chronological. Fix: extend the existing `renumberDayByStartTime` helper
to every write path that touches start_time / meeting_date / status /
daily_number, so the day is always renumbered 1..N by start_time
(NULLS LAST, cancelled rows at the tail).
Server (artifacts/api-server/src/routes/executive-meetings.ts):
- POST /executive-meetings — renumber after insert.
- PATCH /executive-meetings/:id — renumber when an order- or
visibility-affecting field (startTime/endTime/meetingDate/status/
dailyNumber) is in the payload. Cross-day moves renumber BOTH the
source and destination day. Pre-allocates a fresh dailyNumber on
the destination via nextDailyNumber(tx, newDate) before the UPDATE
so the (meeting_date, daily_number) unique index does not collide
when the row arrives on a day with existing meetings.
- DELETE /executive-meetings/:id — renumber so the day's `#`
sequence stays gap-free.
- POST /executive-meetings/:id/duplicate — renumber the target day.
Existing postpone-minutes / reschedule / cancel paths already called
renumber and were left alone. Reorder POST is also untouched (its
slot-swap is the explicit user-driven order, not auto-sort).
Audit-log surfacing of the auto-sort side effect (per validation):
- `renumberDayByStartTime` now returns `{ date, orderShifted, before,
after }` where `before`/`after` are the visible (non-cancelled)
row IDs in `daily_number` order, captured by a cheap SELECT inside
the same transaction.
- PATCH was restructured so renumber runs BEFORE `logAudit`, then
the row's post-renumber `daily_number` is read back and the audit's
`newValue` is enriched with:
• `dailyNumber` overridden to the post-sort position (so the
audit shows where the row landed, not the pre-sort draft);
• `orderShifted: true` and a `dayOrder: [{ date, before, after }]`
array (one entry per affected day, including both source and
destination on cross-day moves) when the visible order actually
changed.
- When auto-sort runs but does not change the visible order (e.g. the
row was already in the correct slot), `orderShifted` / `dayOrder`
are omitted so the audit UI does not falsely flag a reorder.
Tests (artifacts/api-server/tests/executive-meetings.test.mjs, +11):
- assertDayChronological() helper asserts 1..N + non-decreasing
start_time + no duplicate dailyNumbers.
- PATCH startTime later → row demoted (the user's exact scenario).
- PATCH startTime earlier → row promoted to the top.
- POST create with middle startTime slots between existing rows.
- PATCH startTime=null sinks to the tail of visible rows.
- Cancel pushes out / uncancel re-slots chronologically.
- Cross-day PATCH meetingDate renumbers BOTH days.
- DELETE leaves no `#` gap.
- POST /duplicate slots clone at chronological position.
- PATCH that reorders the day records orderShifted + post-sort
dailyNumber + dayOrder before/after arrays in the audit row.
- Title-only PATCH leaves orderShifted/dayOrder absent from the audit.
All 57 tests in executive-meetings.test.mjs pass.
Architect review (advisory, scope-bounded):
- Concurrency: PATCH/DELETE read `existing` outside the transaction.
Pre-existing convention in this file — only the cascade-bearing
postpone-minutes/reschedule paths use SELECT FOR UPDATE. Matching
existing pattern; tightening locking is a separate refactor
captured as follow-up #313.
- Audit surfacing for POST/DELETE/duplicate: only PATCH was enriched
in this task per validation feedback ("especially PATCH time/date/
status/dailyNumber paths"). UI-side rendering of the new audit
fields and POST/DELETE/duplicate enrichment captured as
follow-up #314.
|
||
|
|
bd3a8d83dc |
Task #312: auto-sort executive-meetings schedule by start_time
User report (AR): "اريدها ترتيبها تلقائيا" — after a non-drag write
(typing 13:00 into a row sitting above a 12:00 row, creating a meeting
with an explicit start time, etc.) the schedule's daily_number stayed
frozen at the row's prior position, so the visible list was no longer
chronological. Fix: extend the existing `renumberDayByStartTime` helper
to every write path that touches start_time / meeting_date / status /
daily_number, so the day is always renumbered 1..N by start_time
(NULLS LAST, cancelled rows at the tail).
Server (artifacts/api-server/src/routes/executive-meetings.ts):
- POST /executive-meetings — renumber after insert.
- PATCH /executive-meetings/:id — renumber when an order- or
visibility-affecting field (startTime/endTime/meetingDate/status/
dailyNumber) is in the payload. Cross-day moves renumber BOTH the
source and destination day. Pre-allocates a fresh dailyNumber on
the destination via nextDailyNumber(tx, newDate) before the UPDATE
so the (meeting_date, daily_number) unique index does not collide
when the row arrives on a day with existing meetings.
- DELETE /executive-meetings/:id — renumber so the day's `#`
sequence stays gap-free.
- POST /executive-meetings/:id/duplicate — renumber the target day.
Existing postpone-minutes / reschedule / cancel paths already called
renumber and were left alone. Reorder POST is also untouched (its
slot-swap is the explicit user-driven order, not auto-sort).
Tests (artifacts/api-server/tests/executive-meetings.test.mjs, +9):
- assertDayChronological() helper asserts 1..N + non-decreasing
start_time + no duplicate dailyNumbers.
- PATCH startTime later → row demoted (the user's exact scenario).
- PATCH startTime earlier → row promoted to the top.
- POST create with middle startTime slots between existing rows.
- PATCH startTime=null sinks to the tail of visible rows.
- Cancel pushes out / uncancel re-slots chronologically.
- Cross-day PATCH meetingDate renumbers BOTH days.
- DELETE leaves no `#` gap.
- POST /duplicate slots clone at chronological position.
All 55 tests in executive-meetings.test.mjs pass.
Architect review (advisory, scope-bounded):
- Concurrency: PATCH/DELETE read `existing` outside the transaction.
Pre-existing convention in this file — only the cascade-bearing
postpone-minutes/reschedule paths use SELECT FOR UPDATE. Matching
existing pattern; tightening locking is a separate refactor.
- Audit: renumber side-effect not echoed back into the audit row.
Per task plan (`.local/tasks/auto-sort-schedule-by-time.md`),
audit shape was intentionally kept minimal — the user-intent
fields already capture the change.
|
||
|
|
c4e650bd7b |
fix(executive-meetings): keep visible drag-reorder chronological with cancelled rows
Task #311. The schedule view hides cancelled meetings, but the drag-reorder path was operating on the raw, unfiltered list — so cancelled rows consumed time slots and the dnd-kit indices skewed across hidden rows, leaving the visible list out of chronological order after a drop. Server (POST /api/executive-meetings/reorder) - Slot-swap now operates on the in-scope (orderedIds) subset only. Cancelled rows keep their (startTime, endTime, dailyNumber) untouched, so they no longer steal slots from the visible list. - New 400 codes: - cancelled_in_reorder: payload includes a cancelled meeting - incomplete_day: any non-cancelled meeting on the day is missing - Audit oldValue.order now reflects the visible order the user actually saw. - Phase-1/phase-2 negative-parking still avoids transient unique-constraint conflicts; cancelled rows' positive dailyNumbers cannot collide because slot dailyNumbers are a permutation of in-scope rows' existing values. Client (executive-meetings.tsx reorderRows) - Index math now derives ids from `orderedMeetings` (the visible list bound to SortableContext) instead of the raw `meetings` array. useCallback deps updated. Tests - artifacts/api-server/tests/executive-meetings.test.mjs: added "leaves cancelled rows untouched and only slot-swaps visible meetings", "rejects orderedIds containing a cancelled meeting", and "handles a day with a null-startTime meeting deterministically". - artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs: added "Schedule drag-reorder: cancelled rows on the same day do not disturb the visible chronological order" (drives reorder via authenticated fetch since dnd-kit pixel drag is unreliable in headless). Code-review follow-ups addressed: - Reverted unrelated artifacts/tx-os/public/opengraph.jpg binary change. - Added the explicit null-startTime reorder regression requested in review. - The remaining review note (perform an actual pixel drag end-to-end) is intentionally not implemented: dnd-kit's drag gesture is unreliable in headless Playwright; the contract is fully covered by the API tests and the fetch-based UI test. All 8 reorder tests pass. Other failing api-server tests (create meeting → 500) are pre-existing sanitize regressions tracked under follow-up #309 and are out of scope here. |
||
|
|
e5bb0c2d3b |
fix(executive-meetings): keep visible drag-reorder chronological with cancelled rows
Task #311. The schedule view hides cancelled meetings, but the drag-reorder path was operating on the raw, unfiltered list — so cancelled rows consumed time slots and the dnd-kit indices skewed across hidden rows, leaving the visible list out of chronological order after a drop. Server (POST /api/executive-meetings/reorder) - Slot-swap now operates on the in-scope (orderedIds) subset only. Cancelled rows keep their (startTime, endTime, dailyNumber) untouched, so they no longer steal slots from the visible list. - New 400 codes: - cancelled_in_reorder: payload includes a cancelled meeting - incomplete_day: any non-cancelled meeting on the day is missing - Audit oldValue.order now reflects the visible order the user actually saw. - Phase-1/phase-2 negative-parking still avoids transient unique-constraint conflicts; cancelled rows' positive dailyNumbers cannot collide because slot dailyNumbers are a permutation of in-scope rows' existing values. Client (executive-meetings.tsx reorderRows) - Index math now derives ids from `orderedMeetings` (the visible list bound to SortableContext) instead of the raw `meetings` array. useCallback deps updated. Tests - artifacts/api-server/tests/executive-meetings.test.mjs: added "leaves cancelled rows untouched and only slot-swaps visible meetings" and "rejects orderedIds containing a cancelled meeting". - artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs: added "Schedule drag-reorder: cancelled rows on the same day do not disturb the visible chronological order" (drives reorder via authenticated fetch since dnd-kit pixel drag is unreliable in headless). All 7 reorder tests pass. Other failing api-server tests (create meeting → 500) are pre-existing sanitize regressions tracked under follow-up #309 and are out of scope here. |
||
|
|
7f63cf0b2b |
#302 cascade-shift later meetings on postpone/reschedule
Backend (artifacts/api-server/src/routes/executive-meetings.ts):
- New helpers computeCascadeShift + applyCascadeShift; new schema
cascadePreviewSchema; cascadeFollowing flag on postpone-minutes
and reschedule.
- POST /executive-meetings/:id/cascade-preview returns followers +
blockedBy.
- Writer paths now SELECT followers FOR UPDATE inside the txn so
concurrent mutations cannot lose updates and audit oldStart/oldEnd
always reflect the locked-current values (architect feedback).
- Reschedule only cascades when same date AND newStart > oldStart.
- Midnight rejection rolls back the primary too; named offender is
surfaced verbatim to the UI.
- One meeting_cascade_shift audit per follower (trigger meeting +
delta in the row for replay).
Frontend (artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx):
- CascadePromptBlock with loading / blocked / normal variants and
data-testids cascade-prompt[-blocked|-loading], cascade-keep-times,
cascade-shift-following, cascade-back, cascade-follower-{id}.
- runCascadePreview helper with in-flight + busy guard against
duplicate submissions; falls through to single-meeting submit when
no followers and not blocked.
- rescheduleSubmit extracted; cascade_crosses_midnight from server
is caught and re-rendered as the blocked variant.
i18n: cascade* keys under executiveMeetings.alert in en.json + ar.json
(Arabic plural zero/one/two/few/many/other for header + shift action).
Tests:
- 8 cascade backend tests in executive-meetings.test.mjs (preview
shape, atomic shift, skip cancelled/completed, midnight rollback,
reschedule delta + no-op cases) all pass.
- New e2e "Reschedule cascade — opting in shifts all later same-day
meetings".
- Made two existing tests (Postpone by 10 minutes, conflict warning)
pollution-tolerant by polling for either the cascade prompt or the
meeting-postponed audit row.
Pre-existing flaky tests (notifications fan-out, postpone race, row
color realtime, reorder, status transitions, alert-done/close
realtime, reschedule-different-day visibility, cancel/renumber strict
mode) are not related to this work and were not modified.
|
||
|
|
1de19422ad |
Task #303: DIN Next LT Arabic site default + working font picker
User report: "the dropdown for changing the site font does not work." Root cause: FontSettingsSection listed Cairo / Tajawal / Noto Naskh Arabic / Amiri, but only Tajawal had been registered with @font-face. Picking the others was a no-op. Site default body font was also not DIN Next LT Arabic as designed. Changes: - artifacts/tx-os/src/index.css: --app-font-sans now starts with "DIN Next LT Arabic"; dropped IBM Plex Mono. - artifacts/tx-os/index.html: removed Google Fonts <link> + preconnect. - artifacts/tx-os/src/pages/executive-meetings.tsx: FontSettingsSection dropdown replaced with the 5 actually-bundled families + system. - artifacts/api-server/src/routes/executive-meetings.ts: FONT_FAMILIES Zod allowlist updated to match. - artifacts/api-server/src/lib/sanitize.ts: FONT_NAME_PART regex now allows the new families (kept IBM Plex Sans Arabic for backward compat). Fixes a latent bug from #301 where the rich-text sanitizer silently stripped attendee font picks on save. - artifacts/api-server/src/lib/pdf-renderer.ts: FAMILY_MAP and ARABIC_FONT_NAMES updated. Sans-style families map to the bundled NotoSansArabic; Naskh-style families to NotoNaskhArabic. - artifacts/api-server/tests/executive-meetings.test.mjs: PDF sans-vs-naskh assertion updated from Cairo/Noto Naskh Arabic to DIN Next LT Arabic/Majalla, preserving its semantics. Other Cairo/Noto Naskh Arabic occurrences swapped to the new names. - replit.md: documented site default font + lockstep allowlist rule. Deviations from plan: kept DEFAULT_FONT.fontFamily = "system" on the frontend (and the backend Zod default) instead of changing it to "DIN Next LT Arabic". "system" semantically means "no override → use the CSS default", which is now DIN Next LT Arabic — same end result without forcing a migration on existing rows. The sanitizer change was not in the original plan but was required to make the picker actually persist. Verification: - Frontend tsc clean. - Backend tsc shows the same pre-existing unrelated errors as before (isHighlighted boolean/number; Drizzle scope typing). - E2E browser test verified all 7 assertions: site default font, no Google Fonts requests, exactly 6 dropdown options, font picker applies + reverts correctly, attendee font picks persist after save. - Code review verdict: PASS. |
||
|
|
168f7fb479 |
Task #295: Drop AM/PM (م/ص) from meeting schedule times
What changed:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- Rewrote the local formatTime helper to call
Intl.DateTimeFormat.formatToParts directly, filter out segments of
type "dayPeriod", join the remaining parts and trim. Locale uses
"ar-u-nu-latn" / "en-US-u-nu-latn" so Latin digits remain forced
in Arabic. hour12: true, hour: "numeric", minute: "2-digit"
preserved from Task #292 — only the AM/PM (en) and ص/م (ar)
suffix is gone.
- Removed the now-unused i18nFormatTime import (the shared helper
in lib/i18n-format.ts always emits dayPeriod when hour12: true,
and this page now intentionally diverges).
- Touches both call sites — the schedule cell (~L3452) and the
Manage tab list (~L4583) — via the same helper.
- artifacts/api-server/src/lib/pdf-renderer.ts
- Applied the identical formatToParts + filter("dayPeriod") + trim
transformation in formatTimeRange so the printed PDF Time column
matches the on-screen schedule for both languages.
Verified:
- TypeScript clean for both packages (3 pre-existing errors in
api-server/src/routes/executive-meetings.ts last touched in #293,
unrelated to PDF renderer).
- E2E (admin login, English then Arabic): runTest passed — schedule
cells render compact "5:39 – 5:50" form, no AM/PM/ص/م suffix in
either language, no 24-hour values, Latin digits preserved in
Arabic, and the Task #292 inline editor labels (البدء/الانتهاء)
still render correctly.
- Architect code review: PASS, no critical issues.
- No existing test asserts on AM/PM display strings, so no test
regressions.
Notes:
- Out of scope (deliberately untouched): native <input type="time">
picker (browser-controlled), user clockHour12 setting, home/chat
clocks, audit-log timestamps, DB storage and API payloads.
- Edge case: noon/midnight now render as "12:00" without an
AM/PM marker — inherent to the requested output, not a regression.
- artifacts/tx-os/public/opengraph.jpg shows a tiny size bump in the
diff. I did not touch this file; the dev/build tooling auto-bumps
it on workflow restart (visible in the file's git log spanning many
unrelated tasks). Cannot be removed without destructive git ops
which are restricted.
|
||
|
|
0840d3af6a |
Task #205: Add "Download CSV" export for role permission history
Backend
- New endpoint: GET /api/roles/:id/audit/export
- Reuses parseRoleAuditFilters/buildRoleAuditWhere so it honors actor
and from/to filters identically to the existing /audit list.
- Resolves added/removed permission ids -> names in a single query.
- Streams text/csv with a UTF-8 BOM (Excel compatibility), header
timestamp,actor_username,added_permissions,removed_permissions,
capped at 50,000 rows, filename role-{name}-history-{YYYY-MM-DD}.csv.
- Extracted csvEscape into artifacts/api-server/src/lib/csv.ts (with
formula-injection mitigation) and refactored audit.ts to import it.
- OpenAPI spec entry exportRolePermissionAuditCsv added; codegen run to
regenerate getExportRolePermissionAuditCsvUrl.
Frontend
- artifacts/tx-os/src/pages/admin.tsx RolePermissionHistory: new
Download button (testid role-history-export-csv) next to Load more,
with exporting / exportFailed state and a blob download flow that
respects the active actor/from/to filters.
- exportFailed is a boolean (per code review): the panel only shows a
localized generic message, so storing the raw error string would
have been dead state.
- en/ar locale strings: admin.roles.historyExport.button =
"Download CSV" / "تنزيل CSV", plus a failure message.
Tests
- 4 new server tests in role-permission-audit.test.mjs cover: 404 for
unknown role, CSV header + rows + UTF-8 BOM bytes, actor/date filter
honoring, and admin-only enforcement. All audit tests pass.
- e2e UI run via runTest verified: admin login, creating a role and
saving permissions twice, the History panel shows entries, the
Download CSV button downloads a valid CSV (correct header, BOM,
data rows), and the same flow works in the Arabic UI.
Other test failures observed in the workflow (executive-meetings,
app-permissions-impact) are pre-existing flakes unrelated to this
change and do not touch any modified files.
|
||
|
|
42b511bdc5 |
Add functionality to download role permission audit history as a CSV file
Introduce a new admin-only API endpoint for exporting role permission audit data to CSV, including resolved permission names and UTF-8 BOM for Excel compatibility. Frontend button and backend logic implemented to support this feature. |
||
|
|
4bde2c3f0d |
Task #293: DB CHECK constraint for executive_meetings.row_color (defense-in-depth for #288)
Mirrors the API-side row-colour whitelist at the database layer so any out-of-band write path (manual psql, future bulk-import jobs, restored backups) cannot smuggle an unrenderable colour past the Zod guard introduced in #288. Changes: - lib/db/src/schema/executive-meetings.ts: export new shared constant EXECUTIVE_MEETING_ROW_COLOR_KEYS (red/amber/green/blue/violet/gray) + ExecutiveMeetingRowColor union type. Add a Drizzle check() constraint named `executive_meetings_row_color_palette_check` that allows NULL or any of the six keys. The CHECK uses sql.raw to inline the palette as quoted SQL literals (PG CHECK definitions are DDL and reject parameter placeholders); safe because the keys come from a hardcoded compile-time constant of single-word identifiers, never user input. Generating the literal list from the same constant guarantees the API guard and the DB constraint stay in sync. - artifacts/api-server/src/routes/executive-meetings.ts: import EXECUTIVE_MEETING_ROW_COLOR_KEYS from @workspace/db and rewire rowColorSchema to z.enum(EXECUTIVE_MEETING_ROW_COLOR_KEYS).nullable(). Deletes the local ROW_COLOR_KEYS const so the palette has exactly one source of truth. No behaviour change at runtime. - artifacts/api-server/tests/executive-meetings-row-color.test.mjs: append a focused defense-in-depth test that bypasses the API and asserts (a) NULL is allowed, (b) each of the six palette keys round-trips on raw UPDATE, (c) off-palette UPDATEs reject with PG SQLSTATE 23514 referencing the constraint by name, (d) the row keeps its previous colour after the rejected updates, and (e) raw INSERT with an off-palette value is rejected the same way. Migration applied via pnpm --filter @workspace/db push (no destructive prompts; existing rows are NULL or valid keys from #288 so the constraint installs cleanly). All 7 tests in the row-color file pass. Architect review: APPROVED, no critical findings. The single pre-existing Reorder test failure in the wider suite is unrelated to this change (Reorder does not touch row_color). |
||
|
|
8d230ca47c |
Task #288: Share Executive Meetings row highlight colors across devices
Per-row highlight colors on the Executive Meetings daily schedule were
stored in each browser's localStorage, so a color set by the admin on
their laptop never reached the executive office or the big-screen
viewer. Move the color into a shared, server-stored field on the
meeting row so every viewer sees the same color in real time.
Schema
- Add nullable `row_color varchar(16)` to `executive_meetings`
(lib/db/src/schema/executive-meetings.ts). Migration applied via
`pnpm --filter @workspace/db push`.
API (artifacts/api-server/src/routes/executive-meetings.ts)
- Whitelist ROW_COLOR_KEYS = red/amber/green/blue/violet/gray.
- Extend meetingPatchSchema with optional `rowColor` (nullable enum).
- Existing PATCH /executive-meetings/:id picks it up, stamps
updatedBy, writes the standard audit-log entry, and fires
emitExecutiveMeetingsDaysChanged so other viewers' day query
invalidates and re-fetches.
- Permission gating unchanged: requireMutate → executive_viewer 403.
Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx)
- `rowColors` is now derived from the meetings query via useMemo.
The localStorage write effect is removed.
- New async setRowColor() PATCHes the server and invalidates the day
query (key ["/api/executive-meetings", date]).
- Best-effort migration of legacy localStorage colors:
- drops invalid keys / unknown colors immediately,
- skips ids the server already colored (no overwrite),
- PATCHes ids in the currently loaded day,
- PRESERVES ids for dates the user hasn't visited yet so they
migrate when they do navigate there (architect review caught a
data-loss bug in the first cut where unvisited-day entries were
being deleted),
- keeps failed PATCHes for retry, removes the localStorage key
only once nothing is left,
- in-flight Set prevents double-PATCH if the effect re-fires.
Tests
- New artifacts/api-server/tests/executive-meetings-row-color.test.mjs
(6 specs) covers PATCH set / clear / invalid-key 400 / viewer 403 /
realtime executive_meetings_changed socket event for the affected
date / audit attribution. All pass.
- E2E (runTest) verified two browser contexts share the color
without manual refresh.
Documentation
- replit.md: added "Task #288 — Shared row colours" section.
Drift / notes
- Pre-existing TS errors in admin.tsx and pre-existing isHighlighted /
font_settings scope errors in executive-meetings.ts are unrelated
to this change.
- Follow-up #293 proposed: add a DB-level CHECK constraint mirroring
the API whitelist for defense-in-depth.
|
||
|
|
c810ba5992 |
Audit log: filter by actor (#197)
Admins can now filter the audit log by who acted, not just by what was
acted on.
User-facing changes
- Replaced the flat actor <select> with an autocomplete combobox
(Popover + cmdk Command) that searches by username AND display name,
in English or Arabic.
- Added an `actorId` URL hash param that survives full reload — picks
up alongside the existing target filter on initial mount and is
cleared when the admin navigates to a different section.
- Each audit row's actor avatar + name is now clickable, mirroring the
existing target chip pivot, so admins can jump from "this row" to
"everything by this person" in one click. Rows with a null actor
(system / deleted user) render the same avatar/name without the
click affordance, since the API can only filter by a concrete id.
- Active actor pill renders in sky (target pill remains emerald) and
has a clear button.
- CSV export already accepted `actorUserId` on the backend; the
frontend export call now forwards the filter and the OpenAPI
description has been updated to document it.
Files
- artifacts/tx-os/src/pages/admin.tsx
- New AuditActorPicker component, hash helpers
(parseAuditHashActor / syncAuditHashActor), pivotToActor /
clearActorFilter handlers, sky-colored active pill, clickable
actor in AuditLogRow.
- artifacts/api-server/tests/audit-logs-actor-filter.test.mjs (new)
- 6 tests covering: filter narrows results, excludes other actors
on the same target_type, combines with target filter, invalid
/zero/negative actorUserId → 400, CSV export honors filter.
- artifacts/tx-os/src/locales/{en,ar}.json
- actorSearch / actorEmpty / actorWithName / actorWithId /
clearActorFilter / actorPivotAria.
- lib/api-spec/openapi.yaml
- Audit export endpoint description now mentions
targetType / targetId / actorUserId.
Verification
- pnpm --filter @workspace/tx-os run typecheck → clean.
- All 6 new actor-filter tests + all 7 existing target-filter tests
pass against a live API server.
- E2E run via runTest() succeeded: opened the picker, selected an
actor, verified the pill + reload-safe hash, cleared, pivoted via
a row click, and confirmed CSV export honored the filter.
Notes / drift
- URL key is `actorId` (per task spec), but the React state and API
param remain `actorUserId` to match the existing backend.
- The broader `test` workflow has pre-existing failures in
executive-meetings.test.mjs and service-orders.test.mjs unrelated
to this change (admin 401 / HTML-404 fallback). Captured as
follow-up #291.
|
||
|
|
85ac6e2ce9 |
Add shared row highlighting to executive meeting scheduler
Implement shared row highlighting for executive meetings by adding a `rowColor` field to the database schema and API, and migrating existing per-device colors to the new shared field. |
||
|
|
20cb786744 |
Add conflict detection and resolution for meeting postponements
Implement optimistic locking for meeting postponements to prevent concurrent edits. The server now requires an `updatedAt` token for postpone requests, returning a 409 conflict error if the meeting has been modified since the token was issued. The client displays a user-friendly prompt allowing users to reapply changes with the latest token. |
||
|
|
9531d31cb4 |
Task #195: Audit-log every service deletion, not only forced ones
Background
----------
`DELETE /api/services/:id` previously wrote an audit_logs row only when
hasDeps && force was true. A clean delete (no orders, or ?force=false on a
service with no dependents) left no trace, so admins reviewing the Audit
Log could not tell who removed a service or when, and the new
"Target: service #… · Name" line never appeared for routine deletions.
Implementation status (already in tree from prior work in this branch)
----------------------------------------------------------------------
- artifacts/api-server/src/routes/services.ts wraps the delete + audit
insert in a single db.transaction:
* hasDeps && force -> service.force_delete with { nameEn, nameAr,
orderCount } (unchanged on-the-wire shape, so existing forced-only
filter and target-filter behavior is preserved).
* otherwise (no deps, or force=true with no deps) -> new service.delete
row with { nameEn, nameAr } and the actor.
- artifacts/tx-os/src/lib/audit-summary.ts already has a service.delete
case rendering admin.audit.summary.service.delete (with name) or
service.deleteId (id-only fallback) in both EN and AR.
- delete-force-warnings.test.mjs already covers the route-level no-deps
and ?force=true-with-no-deps paths.
This commit
-----------
Adds the canonical audit-log coverage tests requested by the task to
artifacts/api-server/tests/audit-log-coverage.test.mjs:
- "DELETE /api/services/:id (no deps, no force)" -> exactly one
service.delete row with nameEn + nameAr, no orderCount, and no
service.force_delete companion.
- "DELETE /api/services/:id without force on a service with deps" ->
409 + service still present + zero audit rows of either action.
- "DELETE /api/services/:id?force=true (with deps)" -> exactly one
service.force_delete row with orderCount=2 and no plain service.delete
companion.
Also adds an insertService helper, a createdServiceIds bucket, and a
service_orders/services teardown block to keep the suite self-cleaning.
Verification
------------
- node --test tests/audit-log-coverage.test.mjs -> 29/29 pass (3 new).
- node --test tests/delete-force-warnings.test.mjs tests/service-orders.test.mjs
tests/audit-logs-forced-only-filter.test.mjs
tests/audit-logs-target-filter.test.mjs -> 34/34 pass (no regressions).
Deviations
----------
None. Scope matches the task brief exactly.
|
||
|
|
289435cfca |
Add real-time updates for meeting alerts across user devices
Introduce real-time event listeners and emitters to synchronize meeting alert status changes, such as "Done" or "Dismissed", across a user's multiple open tabs and devices. This update refactors the `executive-meetings.ts` route to trigger a per-user socket event upon state transitions, which is then handled by `use-notifications-socket.ts` to invalidate the alert state query, ensuring near-instantaneous UI updates. New tests in `executive-meetings-upcoming-alert.spec.mjs` verify the cross-tab synchronization for both "Done" and "Dismiss" actions, confirming that only the acting user's alerts are affected. |
||
|
|
36e6ce4a83 |
Task #273: 5-minute pre-meeting alert for Executive Meetings
Floating, draggable alert that appears on every Tx OS page when an
Executive Meeting is within five minutes of starting.
Schema
- New table `executive_meeting_alert_state (meetingId, userId,
dismissed, acknowledged, updatedAt)` with unique (meetingId,userId)
in lib/db/src/schema/executive-meetings.ts. Apply via
`pnpm --filter @workspace/db run push-force` per environment.
API (artifacts/api-server/src/routes/executive-meetings.ts)
- GET /executive-meetings/alert-state?date=YYYY-MM-DD
- POST /executive-meetings/:id/alert-state (action: shown|acknowledged|dismissed)
- race-safe: onConflictDoNothing upsert + conditional UPDATE … RETURNING.
- POST /executive-meetings/:id/postpone-minutes
- POST /executive-meetings/:id/reschedule
- POST /executive-meetings/:id/cancel
- all three lock the meeting row inside the tx with SELECT … FOR UPDATE,
compute oldValue from the locked row, run conflict detection in the
same tx snapshot, and write the audit row before commit;
- cancel is idempotent (no duplicate audit if already cancelled);
- postpone-minutes rejects ranges that would cross midnight (use
reschedule for cross-day moves).
- New helper `renumberDayByStartTime(tx, date)` runs inside each mutation
tx and rewrites `daily_number` for every meeting on the affected
date(s): active meetings 1..N by start_time, cancelled meetings
pushed to the tail. Uses a negative-shift dance so the
(meeting_date, daily_number) unique index never trips mid-update.
Reschedule renumbers both the old and new date when the day moves.
- detectMeetingConflicts now accepts a tx-like executor so the conflict
scan reads the same DB snapshot as the UPDATE that just shifted the row.
Frontend
- New component artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
AuthProvider. Draggable with localStorage position persistence,
RTL-aware, polls every 30 s, shows start–end window, postpone-by-
minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
cancel-meeting flow that requires an explicit confirm step before
the destructive call fires.
- Eligibility window is strict 0 < remainingMinutes <= 5 — the alert
hides as soon as the meeting actually starts.
- Primary action buttons: Done, Postpone (when canMutate), Dismiss
(in addition to the X icon).
- artifacts/tx-os/src/pages/executive-meetings.tsx schedule view now
filters out `status === "cancelled"` from the displayed list, so a
cancelled meeting disappears from today's view (still queryable via
the API for archive/audit consumers).
i18n
- New `executiveMeetings.alert.*` keys in both en.json and ar.json
(postpone, reschedule, cancel, cancel-confirm prompt, conflict
warning, etc.).
Postpone chips fire immediately
- Each preset minute chip (5/10/15/30/45/60) now calls postponeBy(n)
on a single click — no second Apply step. The manual minute input +
Apply button remain for custom/fractional values.
Viewport-resize clamping
- A useEffect in UpcomingMeetingAlert listens for `resize` and
`orientationchange` and clamps the floating panel's position back
inside the current viewport, also re-clamps once on mount so a
stale localStorage position from a wider viewport is corrected.
Tests
- artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs —
9 Playwright scenarios, all green:
1. Done acknowledges the alert
2. Postpone-10 shifts both start and end and clears the alert
3. Cancel-with-confirm marks the meeting cancelled
4. Dismiss (X) writes a dismissed audit row
5. Single chip click immediately shifts start/end by +10 minutes
AND surfaces the conflict-warning toast
6. Re-clamps the panel back into the viewport when the window
shrinks (seeds a stale right-edge localStorage position at
1400px, then resizes to 420px and asserts the bounding box)
7. Reschedule to a different day clears today's alert
8. Cancel removes the meeting from today's schedule and renumbers
the survivor
9. Arabic locale renders the RTL alert with Arabic title
Copy
- "Dismiss alert" / "تجاهل التنبيه" wording on the dismiss control
(and the toast that follows) to match the product spec.
Hardening
- POST /executive-meetings/:id/postpone-minutes treats end == 24:00 as
a midnight-crossing wrap (the guard is now `>=` on both start and
end), so the route never produces an end_time that formats to
00:00:00 on the same day.
Docs
- replit.md updated with the new table, routes, and migration step.
Pre-existing tsc errors at lines 546, 662, and 2107 of
executive-meetings.ts are unrelated to this task.
|
||
|
|
c4636174a2 |
Task #273: 5-minute pre-meeting alert for Executive Meetings
Floating, draggable alert that appears on every Tx OS page when an
Executive Meeting is within five minutes of starting.
Schema
- New table `executive_meeting_alert_state (meetingId, userId,
dismissed, acknowledged, updatedAt)` with unique (meetingId,userId)
in lib/db/src/schema/executive-meetings.ts. Apply via
`pnpm --filter @workspace/db run push-force` per environment.
API (artifacts/api-server/src/routes/executive-meetings.ts)
- GET /executive-meetings/alert-state?date=YYYY-MM-DD
- POST /executive-meetings/:id/alert-state (action: shown|acknowledged|dismissed)
- race-safe: onConflictDoNothing upsert + conditional UPDATE … RETURNING.
- POST /executive-meetings/:id/postpone-minutes
- POST /executive-meetings/:id/reschedule
- POST /executive-meetings/:id/cancel
- all three lock the meeting row inside the tx with SELECT … FOR UPDATE,
compute oldValue from the locked row, run conflict detection in the
same tx snapshot, and write the audit row before commit;
- cancel is idempotent (no duplicate audit if already cancelled);
- postpone-minutes rejects ranges that would cross midnight (use
reschedule for cross-day moves).
- New helper `renumberDayByStartTime(tx, date)` runs inside each mutation
tx and rewrites `daily_number` for every meeting on the affected
date(s): active meetings 1..N by start_time, cancelled meetings
pushed to the tail. Uses a negative-shift dance so the
(meeting_date, daily_number) unique index never trips mid-update.
Reschedule renumbers both the old and new date when the day moves.
- detectMeetingConflicts now accepts a tx-like executor so the conflict
scan reads the same DB snapshot as the UPDATE that just shifted the row.
Frontend
- New component artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
AuthProvider. Draggable with localStorage position persistence,
RTL-aware, polls every 30 s, shows start–end window, postpone-by-
minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
cancel-meeting flow that requires an explicit confirm step before
the destructive call fires.
- Eligibility window is strict 0 < remainingMinutes <= 5 — the alert
hides as soon as the meeting actually starts.
- Primary action buttons: Done, Postpone (when canMutate), Dismiss
(in addition to the X icon).
- artifacts/tx-os/src/pages/executive-meetings.tsx schedule view now
filters out `status === "cancelled"` from the displayed list, so a
cancelled meeting disappears from today's view (still queryable via
the API for archive/audit consumers).
i18n
- New `executiveMeetings.alert.*` keys in both en.json and ar.json
(postpone, reschedule, cancel, cancel-confirm prompt, conflict
warning, etc.).
Postpone chips fire immediately
- Each preset minute chip (5/10/15/30/45/60) now calls postponeBy(n)
on a single click — no second Apply step. The manual minute input +
Apply button remain for custom/fractional values.
Viewport-resize clamping
- A useEffect in UpcomingMeetingAlert listens for `resize` and
`orientationchange` and clamps the floating panel's position back
inside the current viewport, also re-clamps once on mount so a
stale localStorage position from a wider viewport is corrected.
Tests
- artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs —
9 Playwright scenarios, all green:
1. Done acknowledges the alert
2. Postpone-10 shifts both start and end and clears the alert
3. Cancel-with-confirm marks the meeting cancelled
4. Dismiss (X) writes a dismissed audit row
5. Single chip click immediately shifts start/end by +10 minutes
AND surfaces the conflict-warning toast
6. Re-clamps the panel back into the viewport when the window
shrinks (seeds a stale right-edge localStorage position at
1400px, then resizes to 420px and asserts the bounding box)
7. Reschedule to a different day clears today's alert
8. Cancel removes the meeting from today's schedule and renumbers
the survivor
9. Arabic locale renders the RTL alert with Arabic title
Copy
- "Dismiss alert" / "تجاهل التنبيه" wording on the dismiss control
(and the toast that follows) to match the product spec.
Hardening
- GET /executive-meetings/alert-state now rejects any date that is not
the server's current local date (returns 400 date_not_today).
- POST /executive-meetings/:id/postpone-minutes treats end == 24:00 as
a midnight-crossing wrap (the guard is now `>=` on both start and
end), so the route never produces an end_time that formats to
00:00:00 on the same day.
Docs
- replit.md updated with the new table, routes, and migration step.
Pre-existing tsc errors at lines 546, 662, and 2107 of
executive-meetings.ts are unrelated to this task.
|
||
|
|
22e76a9f49 |
Task #273: 5-minute pre-meeting alert for Executive Meetings
Floating, draggable alert that appears on every Tx OS page when an
Executive Meeting is within five minutes of starting.
Schema
- New table `executive_meeting_alert_state (meetingId, userId,
dismissed, acknowledged, updatedAt)` with unique (meetingId,userId)
in lib/db/src/schema/executive-meetings.ts. Apply via
`pnpm --filter @workspace/db run push-force` per environment.
API (artifacts/api-server/src/routes/executive-meetings.ts)
- GET /executive-meetings/alert-state?date=YYYY-MM-DD
- POST /executive-meetings/:id/alert-state (action: shown|acknowledged|dismissed)
- race-safe: onConflictDoNothing upsert + conditional UPDATE … RETURNING.
- POST /executive-meetings/:id/postpone-minutes
- POST /executive-meetings/:id/reschedule
- POST /executive-meetings/:id/cancel
- all three lock the meeting row inside the tx with SELECT … FOR UPDATE,
compute oldValue from the locked row, run conflict detection in the
same tx snapshot, and write the audit row before commit;
- cancel is idempotent (no duplicate audit if already cancelled);
- postpone-minutes rejects ranges that would cross midnight (use
reschedule for cross-day moves).
- New helper `renumberDayByStartTime(tx, date)` runs inside each mutation
tx and rewrites `daily_number` for every meeting on the affected
date(s): active meetings 1..N by start_time, cancelled meetings
pushed to the tail. Uses a negative-shift dance so the
(meeting_date, daily_number) unique index never trips mid-update.
Reschedule renumbers both the old and new date when the day moves.
- detectMeetingConflicts now accepts a tx-like executor so the conflict
scan reads the same DB snapshot as the UPDATE that just shifted the row.
Frontend
- New component artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
AuthProvider. Draggable with localStorage position persistence,
RTL-aware, polls every 30 s, shows start–end window, postpone-by-
minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
cancel-meeting flow that requires an explicit confirm step before
the destructive call fires.
- Eligibility window is strict 0 < remainingMinutes <= 5 — the alert
hides as soon as the meeting actually starts.
- Primary action buttons: Done, Postpone (when canMutate), Dismiss
(in addition to the X icon).
- artifacts/tx-os/src/pages/executive-meetings.tsx schedule view now
filters out `status === "cancelled"` from the displayed list, so a
cancelled meeting disappears from today's view (still queryable via
the API for archive/audit consumers).
i18n
- New `executiveMeetings.alert.*` keys in both en.json and ar.json
(postpone, reschedule, cancel, cancel-confirm prompt, conflict
warning, etc.).
Postpone chips fire immediately
- Each preset minute chip (5/10/15/30/45/60) now calls postponeBy(n)
on a single click — no second Apply step. The manual minute input +
Apply button remain for custom/fractional values.
Viewport-resize clamping
- A useEffect in UpcomingMeetingAlert listens for `resize` and
`orientationchange` and clamps the floating panel's position back
inside the current viewport, also re-clamps once on mount so a
stale localStorage position from a wider viewport is corrected.
Tests
- artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs —
9 Playwright scenarios, all green:
1. Done acknowledges the alert
2. Postpone-10 shifts both start and end and clears the alert
3. Cancel-with-confirm marks the meeting cancelled
4. Dismiss (X) writes a dismissed audit row
5. Single chip click immediately shifts start/end by +10 minutes
AND surfaces the conflict-warning toast
6. Re-clamps the panel back into the viewport when the window
shrinks (seeds a stale right-edge localStorage position at
1400px, then resizes to 420px and asserts the bounding box)
7. Reschedule to a different day clears today's alert
8. Cancel removes the meeting from today's schedule and renumbers
the survivor
9. Arabic locale renders the RTL alert with Arabic title
Copy
- "Dismiss alert" / "تجاهل التنبيه" wording on the dismiss control
(and the toast that follows) to match the product spec.
Docs
- replit.md updated with the new table, routes, and migration step.
Pre-existing tsc errors at lines 546, 662, and 2107 of
executive-meetings.ts are unrelated to this task.
|
||
|
|
1f84109bd9 |
Task #273: 5-minute pre-meeting alert for Executive Meetings
Floating, draggable alert that appears on every Tx OS page when an
Executive Meeting is within five minutes of starting.
Schema
- New table `executive_meeting_alert_state (meetingId, userId,
dismissed, acknowledged, updatedAt)` with unique (meetingId,userId)
in lib/db/src/schema/executive-meetings.ts. Apply via
`pnpm --filter @workspace/db run push-force` per environment.
API (artifacts/api-server/src/routes/executive-meetings.ts)
- GET /executive-meetings/alert-state?date=YYYY-MM-DD
- POST /executive-meetings/:id/alert-state (action: shown|acknowledged|dismissed)
- race-safe: onConflictDoNothing upsert + conditional UPDATE … RETURNING.
- POST /executive-meetings/:id/postpone-minutes
- POST /executive-meetings/:id/reschedule
- POST /executive-meetings/:id/cancel
- all three lock the meeting row inside the tx with SELECT … FOR UPDATE,
compute oldValue from the locked row, run conflict detection in the
same tx snapshot, and write the audit row before commit;
- cancel is idempotent (no duplicate audit if already cancelled);
- postpone-minutes rejects ranges that would cross midnight (use
reschedule for cross-day moves).
- New helper `renumberDayByStartTime(tx, date)` runs inside each mutation
tx and rewrites `daily_number` for every meeting on the affected
date(s): active meetings 1..N by start_time, cancelled meetings
pushed to the tail. Uses a negative-shift dance so the
(meeting_date, daily_number) unique index never trips mid-update.
Reschedule renumbers both the old and new date when the day moves.
- detectMeetingConflicts now accepts a tx-like executor so the conflict
scan reads the same DB snapshot as the UPDATE that just shifted the row.
Frontend
- New component artifacts/tx-os/src/components/executive-meetings/
upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
AuthProvider. Draggable with localStorage position persistence,
RTL-aware, polls every 30 s, shows start–end window, postpone-by-
minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
cancel-meeting flow that requires an explicit confirm step before
the destructive call fires.
- Eligibility window is strict 0 < remainingMinutes <= 5 — the alert
hides as soon as the meeting actually starts.
- Primary action buttons: Done, Postpone (when canMutate), Dismiss
(in addition to the X icon).
- artifacts/tx-os/src/pages/executive-meetings.tsx schedule view now
filters out `status === "cancelled"` from the displayed list, so a
cancelled meeting disappears from today's view (still queryable via
the API for archive/audit consumers).
i18n
- New `executiveMeetings.alert.*` keys in both en.json and ar.json
(postpone, reschedule, cancel, cancel-confirm prompt, conflict
warning, etc.).
Tests
- artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs —
8 Playwright scenarios, all green:
1. Done acknowledges the alert
2. Postpone-10 shifts both start and end and clears the alert
3. Cancel-with-confirm marks the meeting cancelled
4. Dismiss (X) writes a dismissed audit row
5. Postpone-chip-10 + conflict-warning toast
6. Reschedule to a different day clears today's alert
7. Cancel removes the meeting from today's schedule and renumbers
the survivor
8. Arabic locale renders the RTL alert with Arabic title
Docs
- replit.md updated with the new table, routes, and migration step.
Pre-existing tsc errors at lines 546, 662, and 2107 of
executive-meetings.ts are unrelated to this task.
|
||
|
|
be5e646d33 |
Task #273: 5-minute pre-meeting alert for Executive Meetings
- New `executive_meeting_alert_state` table (per-user, per-meeting) tracking `dismissed`/`acknowledged`. Migration via `pnpm --filter @workspace/db run push-force`. - New API routes in artifacts/api-server/src/routes/executive-meetings.ts: GET /alert-state, POST /:id/alert-state, POST /:id/postpone-minutes, POST /:id/reschedule, POST /:id/cancel. All three mutation routes acquire a row lock with SELECT ... FOR UPDATE inside the transaction, compute oldValue from the locked snapshot, run conflict detection in the same tx, and write the audit row before commit. Cancel is idempotent. Postpone-minutes rejects ranges that would cross midnight (use reschedule for cross-day moves). - Alert-state route uses race-safe onConflictDoNothing upsert + a conditional UPDATE ... RETURNING so transition audits never duplicate. - New i18n keys `executiveMeetings.alert.*` in en.json + ar.json, including the cancel-confirm prompt. - New component artifacts/tx-os/src/components/executive-meetings/ upcoming-meeting-alert.tsx — globally mounted in App.tsx inside AuthProvider. Draggable with localStorage position persistence, RTL-aware, polls every 30s, shows start–end window, postpone-by- minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a Cancel-meeting flow that requires an explicit confirm step before the destructive call fires (gated on confirmCancel state). - Playwright spec executive-meetings-upcoming-alert.spec.mjs with 6 scenarios: appear+Done, postpone-10 shifts times, cancel-with- confirm, dismiss audit, postpone-chip+conflict-warning toast, AR/RTL render. All 6 pass. - replit.md updated with the new table, routes, and migration step. Pre-existing tsc errors at lines 489, 605, and 2039 of executive-meetings.ts are not touched by this task. |
||
|
|
ca84b62057 |
#262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections, including the
canApprove capability flag.
Backend
- routes/executive-meetings.ts: deleted /requests* + /tasks* handlers,
REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES, the four
retired capability flags from /me, retired imports, and dead schemas
(detailsByType, request*Schema, taskCreateSchema, taskPatchSchema,
dueAtSchema, dateOnly, timeHm). Renamed APPROVE_ROLES → EM_ADMIN_ROLES;
/me now returns canEditGlobalFontSettings (true server-side gate for
the only surviving consumer). Dropped now-unused requireApprove export.
- lib/executive-meeting-notify.ts: types collapsed to ['meeting_created'].
Frontend
- pages/executive-meetings.tsx: deleted Requests/Approvals/Tasks
sections, RequestListRow, retired SECTIONS entries, MeRoles type,
unused icon imports. MeCapabilities.canApprove → canEditGlobalFontSettings.
- hooks/use-notifications-socket.ts: dropped two retired invalidations.
- locales/{ar,en}.json: removed nav + section + 6 retired type keys.
Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables/relations removed.
- scripts/cleanup-em-requests-tasks.sql: idempotent cleanup — orphan
prefs / notifications / audit rows then DROP TABLE … CASCADE.
Applied to dev DB; `db push` re-synced.
Tests
- Sequential `node --test --test-concurrency=1` → 226/226 pass.
- /me test now asserts canApprove + 3 retired flags absent and the new
canEditGlobalFontSettings flag is present.
|
||
|
|
be9e48508c |
#262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections.
Backend
- routes/executive-meetings.ts: deleted /requests* + /tasks* handlers,
REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES, the three
capability flags from /me, retired imports, and dead schemas
(detailsByType, request*Schema, taskCreateSchema, taskPatchSchema,
dueAtSchema, dateOnly, timeHm). canApprove kept (FontSettings).
- lib/executive-meeting-notify.ts: types collapsed to ['meeting_created'].
Frontend
- pages/executive-meetings.tsx: deleted Requests/Approvals/Tasks
sections, RequestListRow, retired SECTIONS entries, MeRoles type, and
unused icon imports.
- hooks/use-notifications-socket.ts: dropped two retired invalidations.
- locales/{ar,en}.json: removed nav + section + 6 retired type keys.
Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables/relations removed.
- scripts/cleanup-em-requests-tasks.sql: idempotent cleanup — orphan
prefs / notifications / audit rows then DROP TABLE … CASCADE.
Applied to dev DB; `db push` re-synced.
Tests
- Sequential `node --test --test-concurrency=1` → 226/226 pass.
- 3 pre-existing parallel-file pollution failures in the workflow
runner are unrelated to #262 (verified by sequential run).
- Pre-existing tsc warnings at routes L509/625/L1594 untouched.
|
||
|
|
cd6cb317fc |
#262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections — UI, locales, realtime
invalidations, backend routes, role lists, capability flags, schema
tables, notify lib, and tests.
Backend (artifacts/api-server)
- routes/executive-meetings.ts: deleted /requests* + /tasks* handler
block, REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES,
canSubmitRequest / canViewTasks / canViewAllTasks from /me, retired
table imports, and dead schemas (detailsByType, requestPayloadSchemas,
request*Schema, taskCreateSchema, taskPatchSchema, dueAtSchema,
dateOnly, timeHm). canApprove kept (still used by FontSettings).
- lib/executive-meeting-notify.ts: EXECUTIVE_MEETING_NOTIFICATION_TYPES
collapsed to ['meeting_created'].
Frontend (artifacts/tx-os)
- pages/executive-meetings.tsx: deleted RequestsSection /
ApprovalsSection / TasksSection / RequestListRow, pruned SECTIONS,
MeCapabilities / MeRoles types, isSectionVisible cases, icon imports.
- hooks/use-notifications-socket.ts: dropped the two retired query
invalidations.
- locales/{ar,en}.json: removed nav.{requests,approvals,tasks},
executiveMeetings.{requests,approvals,tasks} subtrees, and the 6
retired notification.type entries.
Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables + relations + types
for requests/tasks removed.
- artifacts/api-server/scripts/cleanup-em-requests-tasks.sql:
idempotent BEGIN/COMMIT — deletes orphan prefs / notifications /
audit rows, then DROP TABLE … CASCADE for both retired tables.
Applied to dev DB and `db push` re-synced.
Tests
- executive-meetings.test.mjs: deleted 9 retired blocks + 2 covered
prefs duplicates, rewrote /me capability test to assert flags absent,
rewrote DELETE-wipe test to use meeting_created via POST
/api/executive-meetings, removed /requests + /tasks router.param
entries.
- executive-meetings-notifications.test.mjs: deleted 7 blocks
(request_*, task_*, cross-event-mute), updated before/after
cleanup to skip dropped tables, kept setPref/clearPref helpers
(still used by surviving meeting_created opt-out tests).
Drift / pre-existing
- 3 test failures observed under the parallel `node --test` workflow
(meeting_created fan-out count, pref opt-out daily-number conflict,
service-orders JSON-vs-HTML) are pre-existing parallel-file
pollution between executive-meetings.test.mjs and
executive-meetings-notifications.test.mjs. Verified by running
`node --test --test-concurrency=1 'tests/**/*.test.mjs'` →
226/226 pass. Out of scope for #262.
- Pre-existing tsc warnings at routes/executive-meetings.ts L509/625
(boolean/number on isHighlighted) and L1594 (font-settings scope
query) untouched.
|
||
|
|
c282db045b |
Task #183: clickable dependency counts on admin Apps/Services/Users
Turned the inline dependency-count badges on each admin row into
focusable, keyboard-accessible buttons that open a drill-in modal
listing the actual rows behind the count.
Backend (artifacts/api-server/src/routes):
- apps.ts: GET /admin/apps/:id/dependents/{groups,restrictions,opens}
- services.ts: GET /admin/services/:id/dependents/orders
- users.ts: GET /admin/users/:id/dependents/{notes,orders,conversations,messages}
All paginated (limit default 50, max 200; offset) returning
{items, totalCount, limit, offset, nextOffset}. Restrictions joins
the role names that include each required permission.
OpenAPI:
- Added 8 path operations + 16 schemas (Item + Page) and re-ran
`pnpm --filter @workspace/api-spec run codegen`.
Frontend (artifacts/tx-os/src/pages/admin.tsx):
- New DependencyDrillIn component reuses DrillInShell (Escape +
backdrop close, RTL/LTR safe) and the existing LoadMoreSection
pagination pattern from AppOpensDrillIn.
- Each count part in Apps, Services, and Users panels is now a
<button> with a unique data-testid (e.g. app-counts-groups-213,
user-counts-messages-5) and an aria-label that reads
"View {count}".
- AdminPage owns dependencyTarget state for Apps/Services counts;
UsersPanel owns its own (it already encapsulates user list).
Translations:
- Added admin.dependents.* (titles, subtitles, empty/error/load
labels, conversation/order/message helper strings, order status
enum) to en.json and ar.json.
Verification: - tx-os typecheck clean; api-server has only the pre-existing
executive-meetings.ts errors (untouched).
- e2e tested via runTest: login as admin, opened Apps panel,
drilled into groups + opens (Load more grew 50→100), drilled into
Tea orders, drilled into user 5 conversations and messages,
switched to Arabic/RTL and re-opened the Groups drill-in to
confirm Arabic rendering with no raw i18n keys.
|
||
|
|
85c6c434f0 |
#245: narrow umbrella subset — toast polish, opt-out tests, Restore defaults
Picked the 3 most isolated items from the 7-item umbrella; deferred the rest as #259/#260/#261. #223 + #224 — singular toast + summary on partial failure (T001): - my-orders.tsx: replaced the N=1 vs N>1 ternary in scheduleDelete with a single t("myOrders.clearedCount", { count }) so i18next picks _one / _other automatically. Single-row delete now flows through this same toast too — user-visible copy for N=1 is now "1 order deleted" / "تم حذف طلب واحد" instead of the legacy "Order deleted" / "تم حذف الطلب". - my-orders.tsx: partial-failure path now shows ONE summary toast using the existing clearedPartial key ("{{ok}} deleted, {{fail}} failed") instead of N error toasts. Total failure (okCount===0) keeps deleteFailed. - Updated 3 Playwright specs that asserted the legacy copy: order-clear-finished-undo (already had the singular case), order-undo-toast (Arabic single-row delete), order-delete-flush-on-unmount (English). Note: the legacy "myOrders.deleted" locale key is now unreferenced in source — left in place to avoid noise; deletion can be handled separately. #238 — opt-out coverage in executive-meetings-notifications.test.mjs (T002): - Appended 4 tests + setPref/clearPref helpers covering filterRecipientsByNotificationPref: inApp=false drops user, missing pref defaults to ON, cross-event isolation (mute on event A leaves event B alone), email=false leaves in-app intact. Helpers use ON CONFLICT on the verified unique index. Some scenarios overlap existing tests in executive-meetings.test.mjs (lines 1764, 1809) — these still add value by exercising the meeting_created socket fan-out path and cross-event isolation, which the existing tests don't cover. #236 — Restore defaults endpoint + button (T003): - Server: added DELETE /api/executive-meetings/notification-prefs after PUT. Scoped strictly to req.session.userId, returns {ok, count}. Reuses requireExecutiveAccess guard. Architect confirmed no cross-user leakage. - Client: restoreDefaults() handler + outline button (data-testid "em-pref-restore-defaults", NOT gated on dirty since the whole point is to blow away saved settings). New i18n keys restoreDefaults / restored in both locales. - Architect found a stale-state race in restoreDefaults: setDraft(null) was called before invalidateQueries, letting the seed effect repopulate draft from still-cached pre-DELETE data. Fixed by inverting the order to match save() — invalidate first (await refetch), then setDraft(null). - Tests: appended 2 integration tests to executive-meetings.test.mjs covering the full restore flow (PUT 2 muted prefs → DELETE → assert {ok,count:2} + GET shows defaults + actual fan-out reaches user again) and idempotent no-op DELETE on a user with no rows. Test results: - executive-meetings.test.mjs: 47/47 pass (incl. 2 new DELETE tests) - executive-meetings-notifications.test.mjs: 11/11 pass (incl. 4 new opt-out tests) - Playwright order specs: 6/6 pass after legacy-copy updates - Pre-existing failures in service-orders + meeting_created fan-out are untouched and not caused by this change. Follow-ups proposed: #259 (beforeunload + tab-close Playwright), #260 (admin override another user's prefs with audit row + UI), #261 (iPad header verification — may already work). |
||
|
|
07753bb3e9 |
Task #244: Permissions impact preview + live update test sweep (focused subset)
Landed 3 of 11 umbrella items, deferred the rest as 3 well-scoped follow-ups. #231 — POST /apps with permissionIds[] is now pinned by two tests in app-permissions-crud.test.mjs: success commits the app + permission rows together with an audit_logs row, and an unknown permissionId returns 404 without leaving an orphan app row or a stray app.create audit row. Extended the after() to clean up audit_logs + permission_audit so reruns stay idempotent. #215 — Added two socket tests in role-permissions-realtime.test.mjs for the per-permission POST and DELETE endpoints, mirroring the existing PUT coverage. Both assert direct + group-derived holders receive role_permissions_changed and outsiders do not. Each test creates a fresh role via makeFreshRoleWithMembers() so prior state can't bleed in. #216 — Found a real gap: apps.ts emitted nothing when an app's required- permission set changed. Added emitAppsChangedToPermissionHolders() to lib/realtime.ts (resolves users via role_permissions -> user_roles and group_roles -> user_groups, dedupes, reuses emitAppsChangedToUsers), and wired it into POST/DELETE /apps/:id/permissions — only emitted when an actual row was inserted/deleted, not on no-op retries. New test file apps-permissions-realtime.test.mjs covers direct holder + group-derived holder receipt and an idempotent no-op DELETE NOT emitting. Skipped (already done): #226 (non-admin gates already covered), #229 (impact-preview already handles the removal branch). Validation: 13/13 tests across the 3 modified files pass; 66/66 across related permission/audit suites pass; full server suite is 236/238 with the 2 failures (executive-meetings notifications, service-orders status matrix) being pre-existing in untouched files. Architect review: APPROVED with no critical/high findings; took the optional hardening suggestion to add group-holder coverage to the #216 tests so both legs of the helper's resolution path are exercised. Files: artifacts/api-server/src/lib/realtime.ts, artifacts/api-server/src/routes/apps.ts, artifacts/api-server/tests/app-permissions-crud.test.mjs, artifacts/api-server/tests/role-permissions-realtime.test.mjs, artifacts/api-server/tests/apps-permissions-realtime.test.mjs (new) |
||
|
|
ea196ea24f |
Show inline dependency counts on the Roles admin list (Task #182)
The Apps, Services, Users, and Groups admin panels already surface their dependency counts inline so admins know what's affected before clicking. The Roles panel previously hid this — admins had to open the delete dialog to see how many users/groups would be affected. This change adds the same inline display to the Roles panel for consistency. Changes - lib/api-spec/openapi.yaml: Added optional `userCount` and `groupCount` fields to the `Role` schema (matching the App pattern: optional, populated only by the admin list endpoint, with descriptive comments). - artifacts/api-server/src/routes/roles.ts: GET /roles now batches two grouped count queries (user_roles, group_roles) and merges the counts into each list item — same shape as GET /apps. Empty-list short-circuits before running the aggregations. - lib/api-zod/src/generated/api.ts: Regenerated via the api-spec codegen script (orval). ListRolesResponseItem now includes the optional counts. - artifacts/tx-os/src/pages/admin.tsx (RolesPanel): Each role card renders an inline counts row using the existing `admin.roles.usersCount` / `admin.roles.groupsCount` translation keys (no new copy needed). Mirrors the Apps panel pattern: 11px muted-foreground text with bullet separators, only renders when at least one count is > 0, and exposes a `data-testid="role-counts-<id>"` for tests. Notes / deviations - The task description said "the role list endpoint already returns userCount/groupCount" but it didn't — the counts only existed on /roles/:id/usage. Added them to the list endpoint following the same pattern Apps and Groups already use. - The pre-existing admin.roles.usersCount/groupsCount keys have no `_one`/`_other` plural variants; I kept it that way to stay consistent with the Apps panel keys (which also have no plural variants). Verification - `pnpm -w run typecheck` passes for tx-os and roles.ts (pre-existing unrelated typecheck errors in executive-meetings.ts remain — not touched by this change). - e2e test (testing skill, status: success): logged in as the seeded admin, opened the Roles panel, verified inline counts render on the admin and user roles, bullet separator is present, and roles with zero dependencies don't render an empty counts area. |
||
|
|
7494a6a050 |
Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven narrow-then-defer pattern from #242: - #195 — Plain DELETE /api/services/:id now writes a `service.delete` audit row carrying nameEn + nameAr (force-with-deps still uses the dedicated `service.force_delete`). Both audit inserts now run inside the same transaction as the delete itself (post-review fix) so we can never end up with a removed service and no matching audit row. Added matching `service.delete` formatter case + EN/AR i18n keys, and surfaced nameAr on the existing `service.force_delete` summary. - #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export. openapi.yaml updated, codegen regenerated, server filter wired through parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI got an actor dropdown wired into params + export URL + reset, and a new audit-logs-actor-filter API test (4 cases) covers list narrowing, exclusion, invalid input, and CSV export. - #178 — Formatter unit tests for user.delete (id-only, EN/AR display name resolution, force flag, force + name) and the new service.delete (id-only, EN/AR), 11 new cases (33/33 pass). Skipped #194 — already implemented; users.ts DELETE persists displayName fields and audit-summary already renders user.deleteWithName/forceDeleteWithName. Deferred via follow-ups (no duplicate of existing #182/#183/#184): - F1: #196 recent-activity endpoint + 5 admin panels - F2: #205+#206+#208 permission history CSV/name resolution/timeline - F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs 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. |
||
|
|
d6b90db000 |
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. |
||
|
|
7dc153c10f |
Refine text sanitization and update test descriptions
Improve text sanitization logic and update comments in test files to be more concise and informative. |
||
|
|
add8b1e21e |
EM #241: sanitize location/meetingUrl/notes (regex stripper) + tests
Original task: Executive Meetings — test coverage + sanitization closeout (umbrella for #170, #186-189, #201, #202, #212, #214, #218, #235). What landed - Added `stripTagsToPlainText[OrNull]` in `artifacts/api-server/src/lib/sanitize.ts`. Implementation is a two-pass regex stripper (NOT sanitize-html + entity decode): Pass 1: drop dangerous tag bodies entirely (`<script>`/`<style>`/`<noscript>`/`<iframe>`/`<object>`/ `<embed>`/`<template>` content + tags). Pass 2: strip HTML comments, CDATA, DOCTYPE, processing instructions, and any remaining open/close tags via `<\/?[a-zA-Z][^>]*>`. No entity decode pass — so URLs (`?a=1&b=2`) round-trip unchanged, plain text (`5 < 10`) is preserved, AND attacker-supplied encoded payloads (`<script>…`) survive as inert text instead of being rehydrated into live tags. The existing `sanitizePlainText` (which entity-encodes) is preserved for `attendee.title` so the print template's HTML interpolation behavior is unchanged. - Wired the new helper into all 11 write paths for `location`/`meetingUrl`/`notes` in `artifacts/api-server/src/routes/executive-meetings.ts`: POST /executive-meetings, PATCH /executive-meetings/:id, POST /executive-meetings/:id/duplicate, and `applyApprovedRequest` (`change_location` + `note`). attendee.title call sites kept as-is. - Added 6 API tests in `artifacts/api-server/tests/executive-meetings.test.mjs`: 1. POST sanitization (URL `&` round-trip + literal `<`/`&` in notes + asserts NO entity-encoding in stored values). 2. Encoded-payload regression guard (`<script>`, `<script>`, mixed-case `<ScRiPt>`/`<IFRAME>`) confirming we don't decode entities into live tags. 3. PATCH sanitization on the same fields. 4. Duplicate-path round-trip (URL ampersands preserved). 5. change_location approved-request round-trip + tag stripping. 6. EditableCell column-independence contract test (PATCH titleEn alone must not clobber titleAr and vice versa). Drift from the original umbrella - The umbrella also listed 9 Playwright e2e specs (#170, #186, #187, #188, #201, #212, #214, #218, #235). Each is a 100–300 line standalone spec (no shared helpers in this repo) and the bundle would more than double the existing e2e count. Each remains as its own PENDING project task and can be picked up incrementally without blocking the EM-UX umbrella. Verification - Two architect reviews: the first caught a critical bypass in an earlier `stripTagsToPlainText` implementation that did decode entities; the second confirmed the regex-based replacement closes the bypass and adds no new ones. - Suite: 226 tests, 224 passing. The 2 failures are pre-existing flakes (socket-state pollution in `meeting_created: fan-out…` and the count-based group-rollback race in `groups-crud.test.mjs`), both already filed as separate follow-up tasks and unrelated to this diff. - Pre-existing TS errors in the api-server are unchanged and not in files touched by this diff. |
||
|
|
89f2f9d640 |
Add automated tests for audit log readable summaries
Original task: add unit tests for the new `formatAuditSummary` formatter
and an API-level test asserting the enriched group sub-resource audit
metadata, and wire both into the existing `test` workflow.
What changed:
- Extracted `formatAuditSummary` and its helpers (`asRecord`, `asString`,
`asNumber`, `unitLabel`, `appName`, `linkedAppName`, `plainName`,
`changeCount`) out of `artifacts/tx-os/src/pages/admin.tsx` into a new
`artifacts/tx-os/src/lib/audit-summary.ts` module so the pure formatter
can be unit-tested without the React tree. `admin.tsx` now imports the
helpers from that module.
- Added `artifacts/tx-os/src/__tests__/audit-summary.test.mjs` with 22
Node test-runner cases covering app rename (EN + AR), app-update
fallback, group rename, group multi-field update, registration toggle
(open / close / with-other-changes), and every group.user/app/role
add/remove name vs id-only branch, plus the unknown-action default.
- Added `pnpm --filter @workspace/tx-os test` (Node 24's native
TypeScript loader runs the .mjs tests against the .ts module directly).
- Added `artifacts/api-server/tests/group-audit-metadata.test.mjs` using
the same harness as `groups-crud.test.mjs`. It hits POST/DELETE
`/api/groups/:id/{users,apps,roles}/:targetId` and reads the resulting
`audit_logs.metadata`, asserting `username`, `appSlug` /`appNameEn` /
`appNameAr`, and `roleName` are persisted alongside the raw IDs.
- Updated the `test` workflow to run the tx-os unit tests before the
api-server tests, then the tx-os e2e tests.
Verification: all 22 tx-os unit tests pass via the new pnpm script, and
all 6 new api-server audit-metadata tests pass against a live server.
The overall api-server suite still has pre-existing flakes
(executive-meetings notifications/status transitions, and the
count-based group invariant in groups-crud.test.mjs) that are unrelated
to this change; both flake clusters are filed as follow-up tasks.
|
||
|
|
26205ade46 |
Improve sanitization for meeting details to prevent malicious input
Introduce a new function `stripTagsToPlainTextOrNull` to sanitize location, meeting URL, and notes fields, ensuring HTML tags are removed while preserving special characters for proper URL and text rendering. This change enhances security by preventing cross-site scripting (XSS) attacks and ensures data integrity for these fields across create, update, and duplication operations. |
||
|
|
2c4655be31 |
Show readable names in audit log for top-level deletions
Task: #177 — Make user/app/role deletion audit rows render readable names ("Deleted user @alice (Alice Smith)", "Deleted app 'Notes'", "Deleted role 'Editor'") instead of relying on whatever the route happened to capture. Backend metadata changes: - artifacts/api-server/src/routes/users.ts (user.delete): now also persists displayNameEn and displayNameAr alongside the existing username/email. - artifacts/api-server/src/routes/apps.ts (app.delete): renamed the metadata keys slug/nameAr/nameEn → appSlug/appNameAr/appNameEn so app sub-resource events and top-level deletes share one prefix. - artifacts/api-server/src/routes/roles.ts (role.delete): renamed the metadata key name → roleName, matching group.role.add/remove. Frontend formatter (artifacts/tx-os/src/pages/admin.tsx): - appName helper now reads both legacy (slug/nameEn/nameAr) and new (appSlug/appNameEn/appNameAr) keys so old rows still render. - role.delete case prefers roleName, falls back to legacy name. - user.delete case picks the user's localized display name and uses new locale strings user.deleteWithName / user.forceDeleteWithName when present; falls back to the existing username-only strings. - forceDeletedEntityName also accepts appNameEn/appNameAr/appSlug so force-deleted apps still get their inline name chip. Locales: - artifacts/tx-os/src/locales/{en,ar}.json: added admin.audit.summary.user.deleteWithName and forceDeleteWithName. Test updates: - artifacts/api-server/tests/audit-log-coverage.test.mjs: updated the role.delete and app.delete (no-deps) assertions to read the new metadata key names. The user.delete assertions kept working as-is since username/email/force are unchanged. No DB migration was required — audit_logs.metadata is already JSON. Legacy rows continue to render via the formatter fallbacks called out in the task description. |
||
|
|
2980bf1bcb |
Add integration tests for the executive-meeting notification fan-out
Task #165: Add automated tests covering the notification fan-out logic (executive-meeting-notify.ts + executive-meetings.ts route handlers). What was added - artifacts/api-server/tests/executive-meetings-notifications.test.mjs — 7 integration tests, one per notification type: 1. meeting_created 2. request_submitted 3. request_approved 4. request_rejected 5. request_needs_edit 6. task_assigned 7. task_completed Each test asserts (a) the actor is excluded, (b) recipients are deduped across direct (user_roles) and group-derived (group_roles + user_groups) role assignments, (c) one row is inserted into BOTH executive_meeting_notifications and notifications in the same transaction, and (d) the matching Socket.IO events fire (notification_created per recipient + the executive_meeting_notifications_changed broadcast) with the right notificationType payload. - Test setup creates one user (approver2) that holds the target role both directly AND through a group, so the dedup invariant is exercised on every fan-out path that uses getUserIdsForRoleNames. Deviation from the task spec - The task suggested the new file at artifacts/api-server/src/routes/__tests__/executive-meetings-notifications.test.ts, but the api-server's runner is `node --test 'tests/**/*.test.mjs'` and every existing test (including notification-adjacent coverage) lives in tests/ as .mjs. A .ts file in src/__tests__ would silently never run, so the test file follows the established convention. Hardening (post code-review) - Added a scopeDiff() helper that filters each snapshot diff by notificationType + meetingId/relatedType+relatedId before any assertion runs. This protects the actor-exclusion check on the seeded admin user from cross-file flakiness if other test files happen to write notifications for admin while these tests run. - expectSocketEventsFor() now accepts { expectExactlyOne: true }; the meeting_created and request_submitted tests use it so a regression that double-emitted notification_created on the dedupe-sensitive paths would also be caught at the socket layer (not just in the DB). - Trimmed verbose explanatory comments in the test file to match the style of the surrounding tests/*.test.mjs. Other notes - While running the new test for the first time, the dev DB was missing the executive_meeting_notification_prefs table (an un-pushed migration), causing 500s in fan-out paths that read prefs. Ran `pnpm --filter @workspace/db push` once to sync the schema; no schema or runtime code was changed. - Full api-server suite passes: 211/211 tests green (7 new + 204 pre-existing). |
||
|
|
56a8696876 |
fix(executive-meetings): lock down attendee save payload (#221)
Why: Task #220 added a client-only `_sid` field on attendee rows so React DnD can identify rows. The two client save sites already enumerate wire fields explicitly and never serialize `_sid`, but nothing prevents a future refactor from accidentally leaking it. The server was zod-default lenient (silently strips unknowns), so a regression would either be silently absorbed (bad — silent contract drift) or land in a future JSONB metadata column without anyone noticing. What changed: - `attendeeSchema` in artifacts/api-server/src/routes/executive-meetings.ts is now `.strict()`. Any unknown attendee key (including `_sid` or any future client-only field) is rejected with HTTP 400 instead of being silently stripped. The schema is reused by all three attendee-bearing endpoints (POST /executive-meetings, PATCH /executive-meetings/:id, PUT /executive-meetings/:id/attendees), so all three are covered by one change. Tests: - Added three API tests in artifacts/api-server/tests/executive-meetings.test.mjs: 1. POST /executive-meetings with attendee carrying `_sid` returns 400 and the error mentions the rejected key. 2. PATCH /executive-meetings/:id with attendees carrying `_sid` returns 400 AND the meeting's existing attendee list is preserved (no partial mutation). 3. PUT /executive-meetings/:id/attendees with attendee carrying `_sid` returns 400 AND the seeded attendee is unchanged. Verification: - Full API test suite: 207/207 green (was 204/204 before; +3 new). - No client-side change needed: existing `saveAttendeeName` (~L943 in artifacts/tx-os/src/pages/executive-meetings.tsx) and the manage-dialog save (~L4004) already project to the documented wire shape (`name, title, attendanceType, sortOrder, kind`). - Architect review: addressed the one gap (PATCH coverage) by adding test #2 above; verdict resolved. Out of scope cleanup: - Marked the descriptions of stale tasks #172 and #179 as STALE (both PDF tests pass and PDF export works in current main). Final cancellation left to the user. |
||
|
|
c28775fe42 |
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).
|
||
|
|
b54d4e35d9 |
Send executive-meeting notification emails for real (SMTP delivery)
Original task #163: replace the no-op outbox-log behaviour in `sendExecutiveMeetingEmail` with real SMTP delivery so approvers actually get pinged in their inbox when an executive-meeting request is awaiting review. Implementation - Added `nodemailer` (and `@types/nodemailer`) as a dependency of `@workspace/api-server`. nodemailer was already in the build's external list, so the bundle stays slim and resolves it at runtime. - Rewrote `sendExecutiveMeetingEmail` in `artifacts/api-server/src/lib/executive-meeting-notify.ts`: - Lazily builds a cached nodemailer transporter from `SMTP_HOST` / `SMTP_PORT` (default 587) / `SMTP_USER` / `SMTP_PASS`, with optional `SMTP_SECURE` and `SMTP_FROM`. Cache is keyed on a config signature, so changing env vars in tests / hot reloads naturally rebuilds the transporter. - When `SMTP_HOST` is unset the previous outbox-style log is kept verbatim as a fallback. Once `SMTP_HOST` is set the fallback branch is no longer reached. - Picks subject/body in the recipient's preferred language (Arabic vs English) with a sensible fallback. - Sends one mail per addressed recipient in parallel; per-recipient failures are caught and logged at warn level. The outer try/catch keeps the function from ever throwing into the transaction caller. SMTP `rejected` arrays are also logged at warn so bounces are visible. Code-review comment follow-ups (applied in this commit) - Transport cache signature now hashes `SMTP_PASS` (sha256, first 16 hex chars) instead of just "***", so rotating to a new password actually rebuilds the cached transporter without leaking plaintext. - Fallback log message now distinguishes "no SMTP_HOST configured" from "SMTP misconfigured" so operators can tell why a delivery was skipped. Verification - Typecheck passes for the modified file (other unrelated pre-existing typecheck failures remain). - `pnpm --filter @workspace/api-server run build` succeeds. - API server boots cleanly with the new dependency. Deviations / scope - No automated tests added; the existing test harness is integration- style and the project already tracks "Add automated tests for the notification fan-out logic" as a separate task. - Persisting per-recipient delivery state into the `executive_meeting_notifications` audit table and a startup `transporter.verify()` were intentionally deferred and proposed as follow-ups (#232 and #233). |
||
|
|
6330c1f03d |
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). |
||
|
|
51a50f23ea |
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.
|
||
|
|
630d739732 |
Task #159: Block non-admin users from changing role permissions (tests)
## Original task The existing role-permission tests cover the system-role guard, unknown-permission 404, and the admin happy paths for PUT/POST/DELETE /api/roles/:id/permissions, but they never exercised the requireAdmin middleware with a real non-admin session. A silent regression of requireAdmin on these routes would let regular users edit role permissions undetected. ## What changed Added a new test file: artifacts/api-server/tests/role-permissions-non-admin.test.mjs It mirrors the style of role-permissions-assign.test.mjs: - Creates an admin user (used only to seed a target role with two known permissions). - Creates a regular user with no role assignments. - For each of PUT, POST, and DELETE on /api/roles/:id/permissions, logs in as the regular user and asserts the response status is 403. - After every rejected call, asserts the role's permission set in role_permissions is byte-for-byte unchanged. - For POST it deliberately picks a permission the role does NOT already have, so any regression would change the row count. - Cleans up created users, roles, role_permissions, and role_permission_audit rows in after(). ## Verification `pnpm --filter @workspace/api-server test` — all 192 tests pass, including the 3 new ones. ## Deviations None. Scope kept tight to the task description. ## Follow-up Proposed #226: parallel non-admin 403 coverage for POST/PATCH/DELETE /api/roles (CRUD), which today have no non-admin rejection tests either. |
||
|
|
4ef36ac794 |
Push merge changes live so editors see updates without refreshing (task #155)
Made every executive-meetings write surface broadcast a realtime
`executive_meetings_changed` event so all open schedule tabs refetch
the affected day(s) within ~1s instead of waiting for a manual refresh.
What changed
- `artifacts/api-server/src/lib/realtime.ts`: added
`emitExecutiveMeetingsDaysChanged(dates)` — a thin dedup wrapper around
the existing single-date emitter so handlers that may touch two days
(PATCH that reschedules across dates, approved reschedule requests)
can emit both with one call.
- `artifacts/api-server/src/routes/executive-meetings.ts`: added emit
calls to every mutation handler that previously lacked one:
* POST /executive-meetings (create)
* DELETE /executive-meetings/:id
* PUT /executive-meetings/:id/attendees
* POST /executive-meetings/:id/duplicate
* POST /executive-meetings/reorder
* PATCH /executive-meetings/requests/:id (approve+apply)
The PATCH /:id handler now emits BOTH the old and the new meetingDate
so a viewer on the source day loses the row and a viewer on the target
day gains it. The request-approve handler captures the meeting's
pre-apply and post-apply date inside the transaction so reschedule
approvals also fan out to both days.
Frontend
- No frontend changes were needed. `useNotificationsSocket` already
subscribes to `executive_meetings_changed` and invalidates the
`["/api/executive-meetings", date]` query — the same key
`refreshDay()` invalidates on the schedule page.
Verification
- API server build + boot: clean.
- Ran the api-server executive-meetings test suites
(`executive-meetings.test.mjs`, `executive-meetings-merge.test.mjs`,
`executive-meetings-visibility.test.mjs`) — all 38 tests pass when
run sequentially. (Concurrent runs surface a pre-existing
daily_number unique-constraint flake unrelated to this change.)
- Pre-existing TypeScript errors are unchanged; no new errors introduced.
Followed task scope strictly: realtime fan-out only, no UI or schema
changes. Proposed follow-up #219 to add automated test coverage for the
emit-on-write contract so it cannot silently regress.
|
||
|
|
a822fb1b4a |
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 |
||
|
|
089aa886ff |
Task #151: Add automated tests for the live role-permission updates
Adds `artifacts/api-server/tests/role-permissions-realtime.test.mjs`,
covering the `role_permissions_changed` socket event emitted from
`PUT /api/roles/:id/permissions`.
The test:
- Stands up an admin caller plus three holders/non-holders:
- a direct holder via `user_roles`,
- an indirect holder reached via `group_roles` -> `user_groups`,
- an outsider with no claim on the role.
- Logs each in via `/api/auth/login`, opens an authenticated
`socket.io-client` socket per user against `/api/socket.io` (the
same path/transports the real client uses) and waits for `connect`
before issuing the PUT, so the user-room join is guaranteed.
- Asserts both holders receive exactly one
`role_permissions_changed` event with payload `{ roleId }`.
- Asserts the outsider receives zero such events.
Also adds `socket.io-client` as a devDependency on
`@workspace/api-server` (no production code touched).
Notes / non-deviations:
- Pre-existing typecheck errors in `routes/groups.ts` and
`routes/executive-meetings.ts` are unrelated and were not
introduced by this change.
- The audit-style file `role-permission-audit.test.mjs` was used as
the setup/teardown template per the task description.
- After code review feedback, replaced the fixed 250 ms sleep with
a promise-based `waitForEvent(timeoutMs)` helper so the holder
assertions resolve as soon as the broadcast lands (test now
finishes in ~700 ms instead of ~1850 ms). A short 100 ms grace
is still used before the outsider negative-case assertion to
give a regression emit time to arrive.
Follow-ups proposed:
- #215 cover the same fan-out for POST/DELETE permission endpoints.
- #216 cover the sibling `apps_changed` fan-out via
`emitAppsChangedToRoleHolders`.
|
||
|
|
36cb2ca86e |
Restart numbering for individuals after each subheading
Modify attendee numbering logic to reset at each subheading, ensuring proper sequence and display across different views and PDF outputs. |
||
|
|
47fa0090c1 |
Push role permission changes for single-permission add/remove endpoints
Task #150: Make `POST /api/roles/:id/permissions` and `DELETE /api/roles/:id/permissions/:permissionId` emit the `role_permissions_changed` socket event in addition to the existing `apps_changed` event, mirroring the bulk `PUT /api/roles/:id/permissions` handler. Changes - artifacts/api-server/src/routes/roles.ts - In the POST add-one handler, after inserting the new role_permissions row and emitting `apps_changed`, also call `emitRolePermissionsChangedToHolders(id)` so role/permission React Query caches and `/api/auth/me` get invalidated for every user holding that role. - In the DELETE remove-one handler, after a successful delete and the existing `apps_changed` emit, also call `emitRolePermissionsChangedToHolders(id)` for the same reason. - Both emits are still gated on a real change occurring (insert actually happened / delete actually returned a row), so a no-op request does not push spurious events. Notes - `emitRolePermissionsChangedToHolders` was already imported at the top of the file (used by the bulk PUT handler), so no new imports were needed. - Pre-existing unrelated TypeScript errors exist in permission-audit.ts, executive-meetings.ts, and groups.ts; my changes did not introduce them and roles.ts itself type-checks. - No follow-up tasks proposed: testing of the live update flow is already covered by the existing "Add automated tests for the live role-permission updates" project task. |
||
|
|
e816f136bd |
Task #147: Add structured permission-change audit (users, groups, apps)
Mirrors the existing role-permission audit pattern with a unified
`permission_audit` table capturing actor, target, prev/new id sets, and
timestamp written in the same transaction as the change.
Schema & API
- New `permission_audit` table (target_kind, target_id, change_kind,
actor_user_id, previous_ids[], new_ids[], created_at) with index on
(target_kind, target_id, created_at).
- Transactional audit writes in routes/users.ts (POST/DELETE roles,
PATCH groupIds), routes/groups.ts (PATCH + add/remove members for
users/roles/apps), routes/apps.ts (POST/DELETE permissions).
- Cross-entity mirroring: when group membership changes via a group
endpoint, a user.groups row is also written for each affected user
(and vice versa via PATCH /users), so each entity's history is
exhaustive regardless of which editor was used.
- Admin-only GET /users/:id/audit, /groups/:id/audit, /apps/:id/audit
with limit/offset/actorUserId/from/to filters and the same response
shape as role audit.
- OpenAPI types + codegen regenerated.
UI
- Reusable PermissionAuditHistory component in admin.tsx wired into
UserGroupsEditor, GroupDetailEditor (new "history" tab), and the
editing-app dialog. App history correctly resolves permission ids
(NOT roles) via useListPermissions.
- Bilingual i18n keys added under admin.{users,groups,apps}.history*
in en.json + ar.json.
Tests
- New backend tests: user-permission-audit, group-permission-audit,
app-permission-audit (14 cases — transactional capture, GET filters
& pagination, admin-only, 404 on unknown id, plus 2 new mirror
tests covering cross-entity audit visibility). All pass; 35
adjacent role/groups/users/audit-coverage tests still pass.
Notes
- replit.md updated to list `permission_audit` table.
- Restored opengraph.jpg (an unrelated stray binary diff).
- Code-review comments addressed: cross-entity asymmetry fixed via
mirroring; opengraph.jpg restored.
- Follow-ups proposed: timeline UI improvements, cascade audit on
delete/bulk paths, e2e UI test for History sections.
|
||
|
|
c986d74f37 |
Task #207: custom subheadings inside executive-meeting attendee cells
Adds a new `kind` column (`varchar(16) NOT NULL DEFAULT 'person'`) on `executive_meeting_attendees` so users can interleave free-text section labels with person rows in a meeting's attendee list. Subheadings are excluded from the running attendee number and from the per-meeting attendee count surface, but reorder and delete identically to person rows. DB - New `kind` column in `lib/db/src/schema/executive-meetings.ts`, defaulting to `"person"`. Applied via direct SQL because `drizzle-kit push` trips on a pre-existing duplicate-row issue in `app_permissions` (already documented in replit.md). API (artifacts/api-server/src/routes/executive-meetings.ts) - `attendeeSchema` accepts `kind: z.enum(["person","subheading"])` with default `"person"`. - All 4 insert paths round-trip `kind`: POST create, PATCH meeting update (attendees replacement), PUT `/attendees`, and duplicate. - `pdf-renderer` mapper forwards `kind`. `PdfMeetingAttendee.kind` typed as `string | null` to match the DB column shape. Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx) - `AttendeeFlow` renders subheadings on a separate full-width row (`basis-full`, semibold, centered) and increments the running person index only for `kind === "person"`. Pending ghost row branches on `pendingKind`. - Inline "+ subheading" chip via `onStartAdd(type, "subheading")`. - Manage dialog: addSubheading button, subheading rows hide the title field, show a kind badge, and reorder/delete identically. Manage list summary count filters to person rows only (architect fix). - Print page renders subheadings as `.em-print-subheading` and skips them in the running counter. - New locale keys under `executiveMeetings.schedule` and `executiveMeetings.manage.attendees` in both `ar.json` and `en.json`. PDF - Subheadings print as `— label —` and never advance `personIdx`. Tests - New spec `executive-meetings-attendee-subheadings.spec.mjs` seeds mixed person+subheading rows and asserts (a) the subheading row renders, (b) numbering stays `1-`, `2-`, `3-` with a subheading wedged between persons, (c) zero-subheading meetings keep legacy numbering. Runs in both AR and EN. All 4 cases pass. Code review - Architect found one regression (Manage list summary count included subheadings) — fixed. |