2322b77b8dd3ae5cfb48dff5ffd3ba2edb2690df
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a2ae8434d |
Update project documentation and code comments to remove platform-specific references
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu Replit-Helium-Checkpoint-Created: true |
||
|
|
4a9ca4bd4a |
tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the schedule time cell) to every other inline editor on executive-meetings.tsx, fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed edits felt sluggish or appeared to silently drop. executive-meetings.tsx * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's DayResponse, applies a per-meeting patch via setQueryData, and returns a rollback closure that re-sets the snapshot on PATCH failure. * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now write the optimistic value before awaiting the network call and rollback() in their catch branches. editable-cell.tsx (3-layer blur hardening) 1. saveEdit() failure now re-opens the editor with the user's typed draft preserved (mirrors #316 time-cell UX) so a server error never forces them to retype. The parent's optimistic cache rollback restores the read-only `value`; the editor itself still holds the draft, so flipping `editing` back on is enough. 2. Outside-pointerdown now flushes saveEdit synchronously (capture phase, before the click reaches its target) instead of waiting on the 100 ms blur timer. Closes the gap where a fast click into another cell triggered a row remount before blur fired. 3. Unmount cleanup flushes any pending blurTimerRef fire-and-forget, a final safety net for the case where a refetch tears down the row before any pointer event arrives. editable-cell.tsx (re-entrancy guard) * `savingRef` short-circuits a second `saveEdit` call so the pointerdown flush + the 100 ms blur timer + Enter/Tab handlers can't race and issue duplicate PATCH/PUT requests for one edit. tests/executive-meetings-keyboard-editing.spec.mjs (+5 tests) * Title repaint <300 ms with PATCH delayed 1500 ms. * Title PATCH 500 rolls back; editor re-opens with the typed draft intact; Escape cancels back to the original; DB unchanged. * Rapid click-switch from cell A to cell B commits BOTH PATCHes to the server (proves the synchronous outside-pointerdown flush). * Rapid Tab traversal from cell A to cell B commits BOTH edits EXACTLY ONCE each (proves the savingRef re-entrancy guard). * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses captured testid so the locator survives the rename). All 5 new + 3 #316 time-editor tests green. Out-of-scope pre-existing failures in the `test` workflow (notifications/postpone-race/row-color) left untouched per plan. |
||
|
|
44b54e1fa8 |
tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the schedule time cell) to every other inline editor on executive-meetings.tsx, fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed edits felt sluggish or appeared to silently drop. executive-meetings.tsx * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's DayResponse, applies a per-meeting patch via setQueryData, and returns a rollback closure that re-sets the snapshot on PATCH failure. * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now write the optimistic value before awaiting the network call and rollback() in their catch branches. editable-cell.tsx (3-layer blur hardening) 1. saveEdit() failure now re-opens the editor with the user's typed draft preserved (mirrors #316 time-cell UX) so a server error never forces them to retype. The parent's optimistic cache rollback restores the read-only `value`; the editor itself still holds the draft, so flipping `editing` back on is enough. 2. Outside-pointerdown now flushes saveEdit synchronously (capture phase, before the click reaches its target) instead of waiting on the 100 ms blur timer. Closes the gap where a fast click into another cell triggered a row remount before blur fired. 3. Unmount cleanup flushes any pending blurTimerRef fire-and-forget, a final safety net for the case where a refetch tears down the row before any pointer event arrives. tests/executive-meetings-keyboard-editing.spec.mjs (+4 tests) * Title repaint <300 ms with PATCH delayed 1500 ms. * Title PATCH 500 rolls back; editor re-opens with the typed draft intact; Escape cancels back to the original; DB unchanged. * Rapid click-switch from cell A to cell B commits BOTH PATCHes to the server (proves the synchronous outside-pointerdown flush). * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses captured testid so the locator survives the rename). All 4 new + 3 #316 time-editor tests green. Out-of-scope pre-existing failures in the `test` workflow (notifications/postpone-race/row-color) left untouched per plan. |
||
|
|
85105faf16 |
tx-os #317: optimistic-cache saves for all inline editors
Extends the optimistic React Query cache pattern (proven in #316 for the schedule time cell) to every other inline editor on executive-meetings.tsx, fixing the "تأخير في الاستجابة وأحياناً ما يحفظ" regression where typed edits felt sluggish or appeared to silently drop. executive-meetings.tsx * New `applyMeetingPatch(meetingId, patch)` helper: snapshots the day's DayResponse, applies a per-meeting patch via setQueryData, and returns a rollback closure that re-sets the snapshot on PATCH failure. * `saveTitle`, `saveAttendeeName`, `saveMerge`, and `setRowColor` now write the optimistic value before awaiting the network call and rollback() in their catch branches. Re-throw behaviour preserved where present so EditableCell still resets its draft on failure. editable-cell.tsx * Hardened blur-commit: the cell now stashes the latest `saveEdit` in a ref and an unmount cleanup flushes any pending blurTimerRef fire-and-forget. Without this, a row remount that lands during the 100 ms blur window (e.g. Tab into the next cell triggering an optimistic refetch) tore down the editor before saveEdit could run and silently dropped the typed change. Optimistic cache writes in the parent make the fire-and-forget safe. tests/executive-meetings-keyboard-editing.spec.mjs (+3 tests) * Title repaint <300 ms with PATCH delayed 1500 ms. * Title PATCH 500 rolls back to original; DB unchanged. * Attendee-name PUT delayed 1500 ms repaints in <300 ms (uses captured testid so the locator survives the rename). Architect review: APPROVED, no concerns. All 3 new + 3 #316 time-editor tests green. Out-of-scope pre-existing failures in the `test` workflow (notifications/postpone-race/row-color) untouched per plan. |
||
|
|
f3154554ff |
Task #316: fix executive-meetings inline time editor snap-back + perceived save latency
Two bugs in the schedule time-cell editor:
1. SNAP-BACK: After saving, re-opening the editor briefly showed
blank inputs because the React Query refetch ran behind the
close-then-reopen sequence and the picker re-seeded from stale
meeting props.
2. PERCEIVED LATENCY: The editor stayed open until the PATCH
round-trip resolved, so a slow network made every save feel
slow even though the data round-trip was the only blocker.
Fixes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- saveTimes: optimistically write the new times into the
["/api/executive-meetings", date] query cache (DayResponse shape:
{ date, meetings: [...] }) so the read-only display repaints
immediately. Snapshot previous payload and roll back on PATCH
failure. Append :00 so the cached shape matches the GET refetch.
- TimeRangeCell.save(): close the editor BEFORE awaiting the PATCH;
set savingRef while saving so the [editing, startSaved, endSaved]
sync effect doesn't overwrite the optimistic draft on the next
render. On failure, re-open with the user's draft intact.
- TimeRangeCell sync effect: bail when savingRef is true OR when
the saved refs match lastSyncedRef, preventing stale prop values
from clobbering the optimistic state mid-flight.
Tests added (artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs):
- snap-back: save 21:30/22:45, immediately re-open, assert picker
shows the saved values (not blank).
- perceived-latency: hold PATCH 1.5s; assert editor closes within
800ms and the read-only display shows optimistic times.
- rollback: 500 the PATCH; assert editor re-opens with the user's
draft intact, read-only display shows the original times after
cancel, DB unchanged.
All 3 new tests pass; the existing time-editor tests continue to
pass (6/6 in the targeted regression sweep).
Plan: .local/tasks/task-316.md
|
||
|
|
45f3d88ac5 |
EM time picker: mobile/iPad-friendly layout + touch targets
Follow-up to task #315. The 3-control picker shipped with desktop- sized inline controls (~22px tall) and a single-row layout that pushed the end picker out of view on a 375px iPhone, leaving the user with no obvious way to fill the second time field. What changed - src/components/time-picker-12h.tsx: bumped the "inline" size from h<22px text-[10px] to h-8 (32px) text-sm/text-xs across hour input, minute input, and AM/PM toggle so a finger tap on mobile/iPad lands cleanly. Added `gap-1.5` between the [hour:minute] group and the AM/PM toggle so a thumb-tap on the toggle doesn't accidentally land on the minute input. Added a visible focus ring color and lightened the placeholder so the empty fields read as "fill me in" rather than already-active. - src/pages/executive-meetings.tsx (TimeRangeCell wrapper): switched from `inline-flex` (single-row, ellipsis on overflow) to `flex flex-wrap items-end gap-x-1.5 gap-y-1`. The end picker now drops onto a second line when the cell can't fit both side-by-side, which is the iPhone case. The "–" separator is hidden on narrow widths (`hidden sm:inline`) since wrapping makes it visually wrong. - Bumped Start/End labels from 10px to 11px font-medium for better at-a-glance scanability of which time field is which — this was the user-reported clarity issue. - Save/cancel icon buttons grew to 32x32 hit areas (was tiny p-0.5 around a 14px icon ≈ 16px clickable). - tests/executive-meetings-keyboard-editing.spec.mjs: added a new test.describe block "Schedule mobile viewport (iPhone 375)" with `test.use({ viewport: { width: 375, height: 667 } })`. The test asserts: (a) all 8 picker sub-controls + save button are visible on iPhone, (b) each interactive control is at least 28px tall (regression guard against shrinking touch targets), (c) a complete tap-driven hour+minute+AM/PM entry on both start and end persists 21:30–22:45 to the DB. Validation - Unit tests: 22/22 pass (no logic changes — only CSS + layout). - E2E: Tab walks (desktop) + compact+PM toggle (desktop) + new iPhone-375 mobile test all pass. - TypeScript: clean. |
||
|
|
4cb8532bb6 |
Task #315: rebuild 12h picker as 3 controls (hour + minute + AM/PM)
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. |
||
|
|
e63a7f8193 |
Task #315: 12h time picker with explicit AM/PM (executive meetings)
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 @). |
||
|
|
b880dd3c37 |
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 two real dnd-kit drag tests driven through the keyboard sensor (Space + ArrowUp + Space on the row's grip handle): - "Schedule drag-reorder: cancelled rows ... do not disturb the visible chronological order" — cancelled row keeps its slot, visible rows end up chronological after the drop. - "Schedule drag-reorder: a null-startTime row drags deterministically and inherits its new slot" — null-time row dragged to top inherits the first chronological slot; the originally-null slot shifts to the row that lands at the bottom. Code-review follow-ups addressed: - Reverted the unrelated artifacts/tx-os/public/opengraph.jpg binary change. - Added the explicit null-startTime regressions at both API and Playwright drag levels per review request. - Replaced the fetch-based UI test with real dnd-kit keyboard-sensor drags to fully exercise the client onDragEnd path including the patched index math. All 8 reorder API tests and both new Playwright drag 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. |
||
|
|
360b231693 |
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 a real dnd-kit drag via the keyboard sensor (Space + ArrowUp x2 + Space on the row's grip handle), exercising the full client onDragEnd path including the patched index math. 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. - Replaced the fetch-based UI test with a real dnd-kit keyboard-sensor drag, covering the client index-mapping path end-to-end. All 8 reorder API tests and the new Playwright drag test 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. |
||
|
|
d366dc076c |
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. |
||
|
|
e4aa4fa280 |
Improve time input functionality and fix related test cases
Replace native time input with a custom text input and parser, enabling support for various time formats and fixing associated end-to-end tests. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4ef80303-e96f-4ad0-ba0f-8fce49b7b340 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/HJW50TH Replit-Helium-Checkpoint-Created: true |
||
|
|
4ace4469eb |
Add Playwright keyboard-editing tests for executive-meetings schedule
Task #139: cover Enter/Esc/Tab keyboard editing of time, title, and attendee inline editors with end-to-end tests that seed via DB and log in via UI as admin/admin123. What this adds - artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs with 7 tests: * Tab into time cell + Enter opens edit + valid times save via Enter (asserts PATCH on /api/executive-meetings/:id and DB row updated) * Time cell Esc restores originals and sends no PATCH * Title cell Enter opens edit and Enter saves via PATCH * Title cell Esc restores original and sends no PATCH * Attendee cell Enter opens edit and Enter saves via PUT on /api/executive-meetings/:id/attendees * Attendee cell Esc restores original and sends no PUT * Tab from title cell reaches the time cell in the same row Implementation notes / deviations - Each test seeds its own future-dated meeting (and attendee where needed) directly through the pg pool with a unique date, then cleans those rows in afterAll — same pattern as the existing schedule-features spec. - After every save the page invalidates the day query and refetches; assertions wait for that GET to land before reading the DOM, which removed an early flaky-timing failure. - The title save test was originally written to do select-all + type. We discovered (via instrumented PATCH inspection) that the EditableCell writes to title_ar vs title_en based on the user's preferredLanguage, not the visible UI direction — admin's preferred language is ar, so saves go to title_ar even when the schedule is rendered LTR. The test now appends a unique suffix and asserts against whichever column actually received the saved HTML, mirroring the schedule-features formatting test. The underlying language-write mismatch is captured as a follow-up. - All 7 tests pass locally (~58s total). The pre-existing failures in the `test` workflow are unrelated PDF tests, not from this work. Code-review feedback applied - Removed two unrelated stray files (artifacts/tx-os/nohup.out and artifacts/tx-os/public/opengraph.jpg) that had no code references. - Scoped both attendee selectors under the seeded row's testid (em-row-:meetingId) so the locator stays unambiguous even if another test ever shares a date. Follow-ups proposed - #201 (test_gaps): Shift+Tab + cross-row + ghost-row keyboard tests - #202 (tech_debt): Save edits to the column matching the visible UI Replit-Task-Id: cbd9619c-1475-43c6-998e-163e8e6ec94a |