- Backend: extend `note_received` socket payload with note snapshot
(title, content, color, sentAt), recipientRowId, senderUserId, and
sender summary so the recipient can render the popup without an extra
fetch.
- Add IncomingNotePopupContext: FIFO queue, dedupe by note id, suppress
events for the user's own sends, auto-clear on logout.
- Add IncomingNotePopup AlertDialog that shows the note (title/content/
color) with a sender chip and Reply / Mark read / Open in Notes /
Dismiss buttons. Acknowledge actions call /notes/:id/read.
- Wire the popup globally in App.tsx (alongside the socket bridge).
- Notes page accepts a `?thread=ID` deep link so the popup's Open and
Reply buttons land on the inbox tab + thread dialog.
- Added bilingual `notes.popup.*` strings in EN + AR (RTL respected).
- New Playwright e2e (`notes-popup-on-receive.spec.mjs`) verifies the
popup appears in a second browser context within seconds, sender
doesn't see their own popup, and dismiss closes it.
The first ship of #315 used a single text input + AM/PM toggle. The
spec (lines 30-32) explicitly mandates THREE controls per field:
hour input (1-12), minute input (00-59), and AM/PM toggle. This
follow-up commit refactors TimePicker12h to that shape while
keeping every behaviour from the first ship intact.
What changed
- src/lib/time-12h.ts:
- Added `splitCanonicalForPicker()` — seeds the 3-control picker's
separate hour and minute inputs from a canonical "HH:mm" value.
- Added `combineSplit(hour, minute, period)` — the new combine
path. Joins "${h}:${m}" and runs the result through
combineTextWithPeriod, preserving every disambiguation rule
(1-12 requires toggle, 0/13-23 unambiguous, embedded marker
overrides toggle).
- The hour field accepts a power-user shortcut: any separator
(":", ".", "-", " ") or AM/PM marker letter or 3-4 digit
compact form in the hour field means "I typed the whole time
here, ignore the minute field". Preserves the existing
e2e tests that fill the entire time string into one input.
- kept formatCanonicalAs12h + combineTextWithPeriod for the
power-user shortcut path.
- src/components/time-picker-12h.tsx: rewrote as 3 controls
(hour input, ":" separator, minute input, AM/PM radiogroup).
Imperative {commit, focus, select} handle still focuses the
hour input. dirtyRef preserved to avoid prop overwrite mid-edit.
periodRef still mirrors state for synchronous commit() reads.
- src/pages/executive-meetings.tsx: added `minuteTestId` props on
both inline editor pickers and both manage-form pickers
(`em-time-{start,end}-minute-${id}`,
`em-form-{start,end}Time-minute`). All other test IDs preserved.
- src/__tests__/time-12h.test.mjs: kept the 14 tests for the
text+period path; added 8 new tests for combineSplit covering
split-mode (1-12 ambiguous, 13-23 unambiguous, midnight, hour
power-user shortcut, bare-hour invalid, etc) — 22 tests pass.
- tests/executive-meetings-keyboard-editing.spec.mjs: updated
the Tab walks test to walk 8 stops (start hour → start minute
→ start AM → start PM → end hour → end minute → end AM →
end PM) and assert both typed values survive the focus
changes. The TYPING_SHAPES helper still works unchanged because
the hour field accepts full time strings via the power-user
shortcut.
- public/opengraph.jpg: reverted to HEAD~1 (was inadvertently
swept into the prior commit by the auto-commit; not part of
this task).
Validation
- Unit tests: 22/22 pass.
- E2E: Tab walks + canonical 24h + no-leading-zero + 12h-with-PM
+ compact+PM-toggle all pass against the new 3-control UI.
- TypeScript: clean.
Replaces native <input type="time"> in the executive-meetings editor
with a TimePicker12h that always requires an explicit AM/PM choice
for any 1–12 hour, fixing the "1:15 silently saved as 01:15 (AM)
when user meant 13:15 (PM)" bug. Display stays compact 12h with no
AM/PM marker (per #292); wire format remains canonical HH:mm 24h.
What ships
- src/lib/time-12h.ts: periodFromCanonical, formatCanonicalAs12h,
combineTextWithPeriod (returns "ambiguous" for any unmarked 1–12
hour without a toggle pick; accepts hours 0 and 13–23 directly
because they are unambiguous 24h shapes; typed marker always
wins over toggle).
- src/components/time-picker-12h.tsx: forwardRef component with
imperative {commit, focus, select} handle, dirtyRef to avoid
prop overwrite mid-edit, dir="ltr" for stable RTL layout, and
Enter-on-toggle = "set AND save".
- Wired into TimeRangeCell inline editor (commit-based save handles
invalid/ambiguous/TimeOrderError as toasts and refocuses).
- Wired into MeetingFormDialog with picker refs + handleSaveClick
validation: Save button now calls commit() on both pickers,
blocks on invalid/ambiguous (toast + focus offending picker),
and passes canonical values straight to the parent's save handler
so a stale React state batch from onChange-suppression cannot
silently persist the previous value.
Tests
- 12 unit tests in src/__tests__/time-12h.test.mjs (incl. new
"hour 0 is unambiguous, accepted without a toggle").
- 17 e2e tests in tests/executive-meetings-keyboard-editing.spec.mjs
(incl. "compact + PM toggle" 0115→13:15 and "Tab walks start
input → start AM/PM toggle → end input → end AM/PM toggle").
- Existing manage-create e2e suite still green.
Locale keys (en + ar)
- executiveMeetings.timeEditor.{am,pm,periodGroupStart,periodGroupEnd}
- executiveMeetings.schedule.timeAmbiguousError
Deviations from plan
- Architect review #1 caught two issues that were both fixed:
(1) combineTextWithPeriod treated hour 0 as ambiguous; now
accepts 00:xx unambiguously.
(2) MeetingFormDialog initially used pickers via value/onChange
only, which left a silent-fallback hole because onChange
suppresses ambiguous drafts. Added imperative refs +
handleSaveClick validation; onSave signature changed to
(committedStart, committedEnd) so the parent's save() can
use the freshly-committed values directly.
- Reverted accidental vite.config.ts typo (@replit/... was missing
the leading @).
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.
Original task: add unit tests for the new `formatAuditSummary` formatter
and an API-level test asserting the enriched group sub-resource audit
metadata, and wire both into the existing `test` workflow.
What changed:
- Extracted `formatAuditSummary` and its helpers (`asRecord`, `asString`,
`asNumber`, `unitLabel`, `appName`, `linkedAppName`, `plainName`,
`changeCount`) out of `artifacts/tx-os/src/pages/admin.tsx` into a new
`artifacts/tx-os/src/lib/audit-summary.ts` module so the pure formatter
can be unit-tested without the React tree. `admin.tsx` now imports the
helpers from that module.
- Added `artifacts/tx-os/src/__tests__/audit-summary.test.mjs` with 22
Node test-runner cases covering app rename (EN + AR), app-update
fallback, group rename, group multi-field update, registration toggle
(open / close / with-other-changes), and every group.user/app/role
add/remove name vs id-only branch, plus the unknown-action default.
- Added `pnpm --filter @workspace/tx-os test` (Node 24's native
TypeScript loader runs the .mjs tests against the .ts module directly).
- Added `artifacts/api-server/tests/group-audit-metadata.test.mjs` using
the same harness as `groups-crud.test.mjs`. It hits POST/DELETE
`/api/groups/:id/{users,apps,roles}/:targetId` and reads the resulting
`audit_logs.metadata`, asserting `username`, `appSlug` /`appNameEn` /
`appNameAr`, and `roleName` are persisted alongside the raw IDs.
- Updated the `test` workflow to run the tx-os unit tests before the
api-server tests, then the tx-os e2e tests.
Verification: all 22 tx-os unit tests pass via the new pnpm script, and
all 6 new api-server audit-metadata tests pass against a live server.
The overall api-server suite still has pre-existing flakes
(executive-meetings notifications/status transitions, and the
count-based group invariant in groups-crud.test.mjs) that are unrelated
to this change; both flake clusters are filed as follow-up tasks.
Replit-Task-Id: 182cd4ed-c55c-43e3-b10b-8147a9611fd4