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.
This commit is contained in:
riyadhafraa
2026-05-01 11:56:14 +00:00
parent 5bd5eb9dc7
commit a72c00fe19
9 changed files with 1705 additions and 0 deletions
+29
View File
@@ -188,6 +188,35 @@ export const executiveMeetingPdfArchivesTable = pgTable(
},
);
// #273: Per-user dismissal/acknowledgment state for the 5-minute
// upcoming-meeting alert. One row per (meetingId, userId). Absence of
// a row means the user has neither acknowledged ("Done") nor dismissed
// ("Delete alert") the alert for that meeting yet, so the alert is
// eligible to fire when the meeting falls inside the 5-minute window.
export const executiveMeetingAlertStateTable = pgTable(
"executive_meeting_alert_state",
{
id: serial("id").primaryKey(),
meetingId: integer("meeting_id")
.notNull()
.references(() => executiveMeetingsTable.id, { onDelete: "cascade" }),
userId: integer("user_id")
.notNull()
.references(() => usersTable.id, { onDelete: "cascade" }),
dismissed: boolean("dismissed").notNull().default(false),
acknowledged: boolean("acknowledged").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(t) => ({
perUserUnique: uniqueIndex(
"executive_meeting_alert_state_meeting_user_unique",
).on(t.meetingId, t.userId),
}),
);
export const executiveMeetingFontSettingsTable = pgTable(
"executive_meeting_font_settings",
{