36e6ce4a83
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.