When a row has a color (e.g. red), the cell borders now use a darker
shade of the same color instead of the default gray (#d1d5db).
PDF renderer (pdf-html-renderer.ts):
- Added ROW_COLOR_BORDER map with darker shades for each color
- Colored rows now get both background-color and border-color inline
- Non-merged rows: # and meeting cells get bg+border, attendees and
time cells get border only (preserving partial coloring behavior)
- Merged rows: all cells get bg+border via coloredStyle
Web UI (executive-meetings.tsx):
- Added 'border' field to ROW_COLOR_OPTIONS with matching darker shades
- tintedCellStyle always applies borderColor when rowBorder exists
- Number cell inline style includes borderColor in both rowBg and
tintBg (current meeting highlight) branches
- Merge cell style includes borderColor in both tint and rowBg branches
- Border color persists even when current-meeting highlight is active
Color mapping:
red: fill #fee2e2 → border #fca5a5
amber: fill #fef3c7 → border #fcd34d
green: fill #dcfce7 → border #86efac
blue: fill #dbeafe → border #93c5fd
violet: fill #ede9fe → border #c4b5fd
gray: fill #f3f4f6 → border #d1d5db
User requested maximum table compression with no gaps between rows.
Changes:
- cellPadX: 8→4, cellPadY: 6→0 (zero vertical padding)
- lineHeight: fontSize*1.5 → fontSize*1.05 (very tight)
- All columns (meeting, attendees, time, #) now center-aligned
- Added lineHeight option to DrawOpts type so draw helpers use the
same metric as row measurement (fixes probe/draw mismatch that
could cause row overlap with tight padding)
- drawMixedLine and drawWrappingLine now respect opts.lineHeight
instead of hardcoded fontSize*1.2, falling back to 1.2 when
lineHeight is not provided (backward compatible)
- Table cell draw calls pass lineHeight to drawWrappingLine
- Both AR and EN PDFs verified valid with logo embedded
- Increased logoBoxSize multiplier from 1.8 to 2.8 so the header logo
renders noticeably larger in the PDF output.
- Changed attendees column text alignment from direction-based (right for
RTL) to "center" so attendee names are centered in their column.
- Both AR and EN PDFs verified: valid PDF output, logo embedded, correct
alignment. Code review PASSED, e2e tests PASSED.
Changes to artifacts/api-server/src/lib/pdf-renderer.ts:
- Reduced cellPadX from 6 to 4 (horizontal cell padding)
- Reduced cellPadY from 3 to 2 (vertical cell padding)
- Tightened lineHeight multiplier from 1.25 to 1.2
- Removed +2 padding buffer from heightOfString measurement loop
- Removed +2 from drawWrappingLine return value
- Tightened drawMixedLine return value (1.25→1.2)
- Consolidated header spacing from two moveDown(0.2+0.4) to one moveDown(0.3)
- Tightened title Y offset multiplier from 1.25 to 1.2
- Fixed row height probe to detect script per-line (matching draw path)
instead of per-cell, preventing measurement/draw mismatch in
mixed Arabic/Latin content now that the +2 cushion is removed
Result: Table rows are compact with no visible gaps between them,
matching the user's reference PDF (attached_assets/rrr1_1777879598338.pdf).
Row colors, cell borders, and footer positioning are unchanged.
Both AR and EN PDFs generate successfully (200 status).
The upcoming meeting alert's details panel was grouping attendees by
the `attendanceType` field, but in practice every attendee is stored
as type=internal regardless of their actual group. Users define groups
via subheading rows in the schedule editor (e.g. "الحضور الخارجي",
"الحضور الداخلي"), but the alert stripped subheadings and lumped
everyone under "داخلي".
Fix: Rewrote the grouping logic in DetailsPanel to walk through the
attendees array in order, using subheading rows as natural group
separators. Each subheading becomes a bold, clearly visible group
header (text-xs font-bold) instead of the old tiny 10px/60% opacity
labels. Person rows are listed under their preceding subheading.
Edge cases handled:
- Persons before any subheading: shown without a group header
- No subheadings at all: flat list without misleading "داخلي" label
- Empty subheading groups (heading with no persons): skipped
- Person count still counts only persons, not subheadings
Changed file:
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
(DetailsPanel component, lines ~1744-1885)
Previously, `computeCurrentMeetingId` returned the first meeting whose
time range contained the current minute, so only one row ever got the
green highlight. When two meetings overlapped (e.g., 9:30–10:00 and
9:45–10:15), only the first was highlighted.
Changed `computeCurrentMeetingId` → `computeCurrentMeetingIds` to
return a `ReadonlySet<number>` of ALL matching meeting ids. Updated the
`useMemo` and the `isCurrent` prop on each row from
`currentMeetingId === m.id` to `currentMeetingIds.has(m.id)`.
A module-level `EMPTY_MEETING_SET` constant avoids allocating a new
empty Set on every tick when no meetings are in progress.
Changed file:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- Lines 369-396: new computeCurrentMeetingIds function
- Line 1092: useMemo now uses computeCurrentMeetingIds
- Line 2782: isCurrent uses .has() instead of ===
The green highlight on the currently-in-progress meeting row would get
stuck when the user locked their iPad, switched apps, or the device
slept. The 60-second setInterval timer that drives `nowTick` is frozen
by the browser during background/sleep states. When the user returned,
the timer wouldn't fire immediately, leaving the highlight on the
old meeting.
Fix: Added a `visibilitychange` event listener alongside the existing
interval timer. When the document becomes visible again, it immediately
calls `setNowTick(Date.now())`, which triggers `computeCurrentMeetingId`
to re-evaluate via the existing useMemo dependency. The listener is
properly cleaned up in the effect's return function.
Changed file:
- artifacts/tx-os/src/pages/executive-meetings.tsx (lines 1065-1084)
Previously, the logo upload field in Font Settings was hidden behind two
conditions: canEditGlobal AND scope === "global". Admin users had to
first switch the scope dropdown to "Global" before the logo upload
appeared, making it very hard to discover.
Changes:
- Frontend (executive-meetings.tsx): Removed `scope === "global"`
condition from logo upload rendering — now shows whenever
`canEditGlobal` is true (admin/executive_office_manager).
- Frontend: Save function now always sends `logoObjectPath` when
`canEditGlobal` is true, regardless of selected scope.
- Backend (executive-meetings.ts): When an admin saves with user scope
and includes logoObjectPath, the logo is written to the global row
(upsert) in the same transaction. This preserves the global-only
semantics while allowing admins to update the logo from any scope.
- Removed the 400 rejection for logoObjectPath on user-scope saves.
- Authorization preserved: only EM_ADMIN_ROLES can trigger logo writes.
Tested:
- API: PATCH with scope=user + logoObjectPath correctly updates global row
- E2E: Logo upload field visible for admin in user scope (confirmed)
- Pre-existing test failures unchanged (PDF font assertion, notification tests)
User compared the rendered PDF against their reference (rrr1) and flagged
three concrete differences. This change addresses all three:
1. Time format — formatTimeRange now joins start/end with U+2011
NON-BREAKING HYPHEN (no surrounding spaces): "9:40‑9:50" instead of
"9:40 – 9:50". The non-breaking hyphen looks identical to ASCII "-"
and prevents PDFKit from wrapping the range across two lines in the
narrow Time column.
2. Date placement — removed the ISO date that was printed under the
header title. Added a new bottom-aligned footer block on the leading
edge (right for AR, left for EN) that prints:
• "مقيد" / "Recorded by" (bold)
• Long-form date "3 May 2026" (always Latin, English month name
even on the AR PDF, mirroring the reference)
The date row is forced to baseDirection="ltr" so RTL bidi doesn't
visually reorder the runs into "May 2026 3" on the AR PDF.
formatLongDate parses the ISO route param in UTC to avoid host-tz
day drift. The footer is extracted into a drawFooter() helper and
called in BOTH the empty-day and populated-day code paths so it
always renders.
3. Title weight — title was already calling drawMixedLine with
weight:"bold" but added explicit confirmation; column proportions
tweaked (6/36/39/19) so the wider Time column doesn't force range
wrapping while keeping enough room for Meeting/Attendees content.
Other touches:
- Added `recordedBy` to PdfLabels type + RenderPdfInput.labels type
and to ar.json/en.json executiveMeetings.pdf blocks.
- Page-break check reserves footer height so the last row never
overlaps the bottom footer.
Verified: PDF Arabic shaping regression test passes; live AR/EN PDFs
match the reference; one-page-fit still holds for typical days.
Pre-existing tsc errors in executive-meetings.ts and unrelated test
suite failures are not caused by this work.
Update font configurations to exclusively use DIN Next LT Arabic, preload necessary font files, and implement eager font loading to prevent display issues during printing.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 814df7a0-9300-4efe-a4b5-b5e474e8c99f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.
Prior mark_task_complete was rejected for three issues. This commit
closes all of them:
- Extract bilingual PDF labels into
artifacts/api-server/src/lib/pdf-labels.ts and import it from the
PDF route. Mirror the same keys under executiveMeetings.pdf.* in
the tx-os ar.json / en.json locales so the frontend stays in
sync.
- Add an end-to-end test that exercises the brand-logo path: sign
an upload URL, PUT the bytes through the storage sidecar, save
the path on the global font-settings row, render the PDF, and
assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
test caught a real silent failure: PDFKit's png-js decoder
rejected the canonical 67-byte base64 "smallest valid PNG", so
the renderer was logging a warning and proceeding without a
logo. The fixture now constructs a real 1x1 RGB PNG inline using
zlib + a CRC32 routine (no new dependencies). Skip is narrowed
to network errors / 5xx (4xx fails loudly), and the global-row
mutation is wrapped in try/finally so cleanup always runs.
- Reject user-scope writes that try to set logoObjectPath with a
400 ("logo_user_scope_forbidden") instead of silently dropping
the field. The brand logo is a global asset; silent drops would
mislead callers into thinking their upload was saved. Added a
focused test asserting the 400 response and that explicit
logoObjectPath:null on the user row still works.
Also removed 12 stray backup/snapshot files at the repo root
(*.old, *-base.{ts,tsx,mjs}, locale snapshots) that were
accidentally tracked in the previous commit.
Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.
Prior mark_task_complete was rejected for three issues. The first
(stray backup files) was a false positive. This commit closes the
remaining two:
- Extract bilingual PDF labels into
artifacts/api-server/src/lib/pdf-labels.ts and import it from the
PDF route so the strings are no longer inlined inside the route
handler. Mirror the same keys under executiveMeetings.pdf.* in
the tx-os ar.json / en.json locales so the frontend stays in
sync.
- Add an end-to-end test that exercises the brand-logo path: sign
an upload URL, PUT the bytes through the storage sidecar, save
the path on the global font-settings row, render the PDF, and
assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
test caught a real silent failure: PDFKit's png-js decoder
rejected the canonical 67-byte base64 "smallest valid PNG", so
the renderer was logging a warning and proceeding without a
logo. The fixture now constructs a real 1x1 RGB PNG inline using
zlib + a CRC32 routine (no new dependencies) and the assertion
passes.
Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
- 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.
Added "PDF content" test that inflates PDFKit FlateDecode streams
and asserts (a) /Title carries the new Arabic label, (b) amber
rowColor paints #fef3c7, (c) #fecaca isHighlighted overlay is
gone, (d) user fontColor reaches body text fills.
- Frontend follow-up: logo preview in FontSettingsSection now uses
resolveServiceImageUrl so /objects/<id> -> /api/storage/objects/<id>
(the URL the API actually serves), matching chat-avatar plumbing.
Type-check clean for api-server and tx-os; new font-settings + PDF
content tests pass. Other test failures in the suite pre-date this
change.
- 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.
- Reorder audit table columns to RTL order: المستخدم، الوقت، الإجراء،
النوع، التغييرات. Drop the entityId ("المعرف") column entirely. Update
loading/empty colSpan from 6 to 5 to match the new column count.
- Rename audit.col.actor copy: "المنفّذ" → "المستخدم" (ar.json) and
"Actor" → "User" (en.json). Underlying data field name unchanged.
- Localize the Changes cell in AuditDiffSummary:
- Field-name labels (titleAr, titleEn, meetingDate, dailyNumber,
startTime, endTime, location, meetingUrl, platform, status,
isHighlighted, notes, assignedTo, taskType, dueAt, reviewDecision,
reviewNotes) now resolve via executiveMeetings.audit.field.* with a
raw-key fallback so unknown future fields don't crash the row.
- Enum values render via existing label namespaces:
`platform` → executiveMeetings.platform.*, `status` →
executiveMeetings.status.*, `isHighlighted` → executiveMeetings.common.yes/no.
- Both the diff view and the single-side created/removed view use the
same labelForKey/fmtValue helpers.
- Added matching audit.field.* keys to ar.json and en.json with sensible
Arabic and English labels.
- No test changes needed: no existing test asserts on the old column
count, the "Actor"/"المنفّذ" header, the entityId column, or raw
English field names in Changes.
- tsc passes for @workspace/tx-os.
- Reorder audit table columns to RTL order: المستخدم، الوقت، الإجراء،
النوع، التغييرات. Drop the entityId ("المعرف") column entirely. Update
loading/empty colSpan from 6 to 5 to match the new column count.
- Rename audit.col.actor copy: "المنفّذ" → "المستخدم" (ar.json) and
"Actor" → "User" (en.json). Underlying data field name unchanged.
- Localize the Changes cell in AuditDiffSummary:
- Field-name labels (titleAr, titleEn, meetingDate, dailyNumber,
startTime, endTime, location, meetingUrl, platform, status,
isHighlighted, notes, assignedTo, taskType, dueAt, reviewDecision,
reviewNotes) now resolve via executiveMeetings.audit.field.* with a
raw-key fallback so unknown future fields don't crash the row.
- Enum values render via existing label namespaces:
`platform` → executiveMeetings.platform.*, `status` →
executiveMeetings.status.*, `isHighlighted` → executiveMeetings.common.yes/no.
- Both the diff view and the single-side created/removed view use the
same labelForKey/fmtValue helpers.
- Added matching audit.field.* keys to ar.json and en.json with sensible
Arabic and English labels.
- No test changes needed: no existing test asserts on the old column
count, the "Actor"/"المنفّذ" header, the entityId column, or raw
English field names in Changes.
- tsc passes for @workspace/tx-os.
- safe-html.ts: add htmlToPlainText (DOMParser, br/block→\n, decode, collapse)
and wrapAsParagraph (HTML-escape + <p>…</p>) helpers.
- executive-meetings.tsx:
- openEdit() runs htmlToPlainText on titleAr, titleEn, and each attendee.name
so the dialog's plain <Input> shows clean text instead of raw markup.
- save() wraps titleAr, titleEn, and each attendee.name with wrapAsParagraph
so the on-disk format stays byte-compatible with what the inline Tiptap
EditableCell produces. Empty-title validation still fires off the plain
string before wrapping.
- Swapped MeetingFormDialog buttons: addSubheading now renders before the
attendee-add button.
- ar.json: executiveMeetings.manage.attendees.add "إضافة حاضر" → "إضافة عضو".
- en.json: same key "Add attendee" → "Add member".
- Inline schedule cell chips (executiveMeetings.schedule.*) untouched.
Introduce new utility functions for HTML to plain text conversion and paragraph wrapping in `safe-html.ts`. Update locale files (`ar.json`, `en.json`) to change "attendee" to "member" in the add button label. Modify `executive-meetings.tsx` to swap the functionality and labels of the "Add attendee" and "Add subheading" buttons.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: edd35722-74e8-4f8d-a3cc-bac6c5dccc51
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
Two small Arabic-only string changes in the Upcoming Meeting alert
popup, requested off a screenshot:
1. Header title: "اجتماع يبدأ قريبًا" → "الاجتماع التالي".
2. Countdown line: replaced "خلال" with "بعد" so the alert reads
"يبدأ بعد 5 دقائق" instead of "يبدأ خلال 5 دقائق". Applied to all
plural forms (one/two/few/many/other). minutesAway_zero ("يبدأ
الآن") and the separate startsNow key were left unchanged.
Files:
- artifacts/tx-os/src/locales/ar.json
- executiveMeetings.alert.title
- executiveMeetings.alert.minutesAway_{one,two,few,many,other}
Out of scope (untouched on purpose):
- en.json — user only asked about the Arabic copy.
- upcoming-meeting-alert.tsx — no logic/layout change needed; it
already reads these keys via i18next.
- All other behavior (blink, drag, buttons, etc.) carried over from
#344.
Verification:
- ar.json still valid JSON; key set unchanged.
- Pre-existing red workflows (api-server, combined `test`) are
unrelated upstream failures and not caused by this change.
The user pointed out that the "بريد إلكتروني" channel they could see in
the per-user "تفضيلات التنبيهات الخاصة بي" card is not part of the
product (no email delivery wired up), so exposing the toggle was
misleading. They sent a screenshot and asked to delete everything in
it: the page heading "التنبيهات", the intro paragraph, and the entire
prefs card (event row + in-app/email switches + Save / Reset / Restore
defaults buttons).
Changes:
- artifacts/tx-os/src/pages/executive-meetings.tsx
- Removed the <h2> heading, the <p> intro, and the
<NotificationPrefsCard /> render call from the notifications page
component. The audit-log table below stays untouched.
- Deleted the NotificationPrefsCard component definition and the
NotificationPref / NotificationPrefsResponse types (only consumers
were inside the deleted card).
- artifacts/tx-os/src/locales/{ar,en}.json
- Removed executiveMeetings.notificationsPage.heading,
executiveMeetings.notificationsPage.intro, and the entire
executiveMeetings.notificationsPage.prefs.* subtree.
- Kept notificationsPage.col.*, type.*, status.*, empty — still used
by the audit-log table.
Out of scope (intentionally not touched):
- Backend /api/executive-meetings/notification-prefs routes and the
email channel in fan-out helpers. The backend defaults are left
intact so notification fan-out keeps working; only the UI surface
is removed.
- Tests: ripgrep showed no references to the removed test-ids
(em-notification-prefs, em-pref-inapp-*, em-pref-email-*,
em-pref-save, em-pref-reset, em-pref-restore-defaults) in the
Playwright/unit suites, so nothing to update.
Verification:
- pnpm -C artifacts/tx-os exec tsc --noEmit clean.
- Switch import retained: still used by row-color popover and another
call site in the same file.
- The api-server workflow and combined `test` workflow are red with
pre-existing upstream failures unrelated to this UI-only change.
Four UI changes requested by an Arabic-speaking user on the floating
"اجتماع يبدأ قريباً" popup:
1. Time window LTR — wrap the header `alert-time-window` span in
`dir="ltr"` so the start time always appears on the left and the
end on the right, even inside the Arabic RTL UI. Prevents the
"م 3:20 – م 3:10" reversal seen in the user's screenshot.
2. Drop the duplicate "Time" row inside DetailsPanel — the same
start/end window is already shown in the header strip. Removed
the `if (timeWindow) rows.push({ key: "time", ... })` block; the
`timeWindow` local was deleted along with it. Header is now the
single source of truth.
3. Remove the labelled "تجاهل التنبيه" footer button. The X close
button in the header still calls `handleDismiss`, so users can
still dismiss; the action bar is now just Done / Postpone /
Details. `handleDismiss`, `dismissToast`, and the i18n keys are
left intact for the X button.
4. Make the "يبدأ خلال N دقائق" / "يبدأ الآن" countdown blink red.
Added a dedicated `@keyframes em-blink` (1s, opacity 1 → 0.15 →
1) and `.em-blink` class in `src/index.css`, with a
`prefers-reduced-motion` override that pins it at full opacity.
Countdown span uses `text-red-600 em-blink font-semibold`. The
previously-used `accentText` local had no remaining consumers,
so it was removed (replaced with an explanatory comment).
Files:
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
- artifacts/tx-os/src/index.css
Verification: `pnpm -C artifacts/tx-os exec tsc --noEmit` clean.
Architect review APPROVED (PASS). No tests referenced the removed
`alert-dismiss` / `alert-details-time` testids.
Pre-existing failures (unrelated): api-server workflow and the
combined `test` workflow continue to fail with the same upstream
issues observed in #342/#343 trajectory; this change does not
touch the API server or e2e test infrastructure.
Native window.confirm exposes the Repl hostname and ignores the app's
RTL/branding. The user (Arabic-speaking) flagged it as visually jarring
inside Executive Meetings.
Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Add a small ConfirmProvider + useConfirm() backed by the existing
shadcn AlertDialog (`@/components/ui/alert-dialog`). The hook returns
Promise<boolean> so call sites stay one-liners:
if (!(await confirm({ message, destructive: true }))) return;
- Single-flight guard: if a second confirm() arrives while the first is
still pending, the previous Promise resolves to false (no orphaned
resolvers).
- Mount the provider once around the page (split the default export
into a thin wrapper + ExecutiveMeetingsPageInner so the inner tree
can call useConfirm()).
- Centered title + description, destructive-styled confirm button,
mobile-safe sizing (max-w-md, w-[calc(100%-2rem)], max-h-[90vh],
overflow-y-auto). Explicit `dir` on AlertDialogContent so the
portal-rendered dialog always inherits the page's RTL/LTR.
- Footer renders Action first then Cancel: in LTR Confirm sits on the
left and Cancel on the right; RTL flips automatically (Confirm right,
Cancel left). Mobile stacks Cancel on top, destructive at the bottom.
- Replace all 5 window.confirm sites:
1. Schedule single-row delete (deleteMeeting)
2. Schedule bulk delete
3. Manage bulk delete
4. Manage single delete (confirmDelete)
5. MeetingFormDialog "remove all attendees" (function turned async)
- Add `confirm` to the relevant useCallback deps (deleteMeeting,
bulk delete) and inject useConfirm() inside ScheduleSection,
ManageSection, MeetingFormDialog (all rendered inside the provider).
- New i18n keys: executiveMeetings.common.confirm and .confirmTitle in
both ar.json and en.json (used as default title / non-destructive
confirm label).
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI change
(also failing before the task started).
Native window.confirm exposes the Repl hostname and ignores the app's
RTL/branding. The user (Arabic-speaking) flagged it as visually jarring
inside Executive Meetings.
Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Add a small ConfirmProvider + useConfirm() backed by the existing
shadcn AlertDialog (`@/components/ui/alert-dialog`). The hook returns
Promise<boolean> so call sites stay one-liners:
if (!(await confirm({ message, destructive: true }))) return;
- Mount the provider once around the page (split the default export
into a thin wrapper + ExecutiveMeetingsPageInner so the inner tree
can call useConfirm()).
- Centered title + description, destructive-styled confirm button,
mobile-safe sizing (max-w-md, w-[calc(100%-2rem)], max-h-[90vh],
overflow-y-auto), centered footer that stacks on small screens via
the existing AlertDialogFooter classes.
- Replace all 5 window.confirm sites:
1. Schedule single-row delete (deleteMeeting)
2. Schedule bulk delete
3. Manage bulk delete
4. Manage single delete (confirmDelete)
5. MeetingFormDialog "remove all attendees" (function turned async)
- Add `confirm` to the relevant useCallback deps (deleteMeeting,
bulk delete) and inject useConfirm() inside ScheduleSection,
ManageSection, MeetingFormDialog (all rendered inside the provider).
Reuses existing translation keys (deleteRowConfirm, bulkDeleteConfirm,
manage.deleteConfirm, attendees.removeAllConfirm). Title/labels fall
back via tWithFallback to Arabic strings if the i18n keys are missing.
Verified `pnpm -C artifacts/tx-os exec tsc --noEmit` is clean.
Pre-existing api-server / test workflow failures are unrelated to this
UI change (also failing before the task started).
User feedback (RTL screenshot): the "إجراءات" header and its three
icon buttons were stuck against the far edge of the column, away
from the new vertical divider added in #337; the "الوقت" header
also sat against the start edge while the short "1:55 — 2:05"
value floated separately, so they didn't visually line up.
- Switches both the Time and Actions header cells from
`text-start` / `text-end` to `text-center`.
- Adds `text-center` to the Time data cell so the
fixed-width "HH:MM — HH:MM" string sits centered under its
header.
- Switches the Actions data cell from `text-end` to
`text-center`; the inline-flex button group (edit /
duplicate / delete) inherits the centering.
- Loading and "no meetings" rows already render the message
via `colSpan={4}` and a separate empty Actions cell; no
text alignment change is needed there for visual parity.
Other columns (select-checkbox, #, Meeting) keep their
existing `text-start` so long titles stay flush with their
header label. The vertical divider (#337) and column widths
(w-28 / w-32) remain untouched. No change to data flow,
testids, or other tabs.
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated.
User feedback: the small grey subtitle under each meeting title in
the Manage tab table read e.g. "4 · none" — the raw `platform`
enum value leaked an English token next to Arabic content for
every meeting that has no virtual platform.
- Drops the ` · {m.platform}` segment from the subtitle in
`ManageSection`'s row rendering inside
`artifacts/tx-os/src/pages/executive-meetings.tsx`. The
attendee count (filtered on `kind === "person"`) stays
exactly as before, with the same `text-xs text-gray-500`
styling so layout is unchanged.
- Adds a short comment explaining why so future readers don't
re-introduce the platform token here. The platform is still
shown elsewhere when meaningful (the "Virtual attendance —
{platformLabel}" group header in AttendeeFlow at L4304/L4402,
and the platform `<Select>` in the edit dialog) — those were
intentionally not touched.
- No change to data model, save/load logic, or testids.
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI
change.
User feedback: in the Manage tab table the action icons looked
glued to the time value because there was no visible boundary
between the Time column and the Actions column.
- Adds `border-s border-gray-200` to both the Actions header
cell (`<th>`) and the per-row Actions data cell (`<td>`) in
the Manage tab table inside `executive-meetings.tsx`.
- Uses the logical `border-s` (inline-start) so the divider
lands on the correct edge of the Actions column in both LTR
and RTL — visually it always sits between Time and Actions.
- Color matches the existing table border (`gray-200`) and the
border extends the full row height, in line with the
surrounding row separators.
- Loading / empty states use a `colSpan={5}` row so the divider
doesn't show there, which is the desired behavior (no
inconsistent half-divider over centered text).
- No change to other columns, no change to Schedule/Audit/
Notifications tables, and no change to colors/widths/testids.
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI
change.
User feedback: in the Manage tab table the action icons looked
glued to the time value because there was no visible boundary
between the Time column and the Actions column.
- Adds `border-s border-gray-200` to both the Actions header
cell (`<th>`) and the per-row Actions data cell (`<td>`) in
the Manage tab table inside `executive-meetings.tsx`.
- Uses the logical `border-s` (inline-start) so the divider
lands on the correct edge of the Actions column in both LTR
and RTL — visually it always sits between Time and Actions.
- Color matches the existing table border (`gray-200`) and the
border extends the full row height, in line with the
surrounding row separators.
- Loading / empty states use a `colSpan={5}` row so the divider
doesn't show there, which is the desired behavior (no
inconsistent half-divider over centered text).
- No change to other columns, no change to Schedule/Audit/
Notifications tables, and no change to colors/widths/testids.
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this UI
change.
User feedback: "سو فريز هنا لأني نزلت تحت وما شفتها" — the
Manage tab's bulk-action toolbar (added in #332) was not
visible after scrolling down through the meetings list.
- Adds a `headingRef` + `ResizeObserver` effect in
`ManageSection` that publishes the live height of
`em-manage-heading-bar` as a `--em-manage-heading-h` CSS
var on the `[data-em-root]` element. Mirrors the Schedule
tab's existing pattern at L2155 for `--em-heading-h`.
Distinct var name avoids collision when switching tabs;
cleared on unmount so the Schedule tab doesn't inherit a
stale offset.
- Updates `em-manage-bulk-toolbar` to `position: sticky`
with `top: calc(var(--em-header-h) + var(--em-manage-heading-h))`,
matching horizontal bleed (`-mx-3 sm:-mx-6 px-3 sm:px-6`)
and `#f4f6fb` page-bg of the heading bar so the two visually
stack as a single header unit. Drops the rounded card style,
uses a bottom-border + drop-shadow for the seam.
- Adds `print:hidden` for parity with the Schedule
counterpart so the toolbar doesn't leak into PDF exports.
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this
change. No behavior change to the Schedule tab.
User feedback: "سو فريز هنا لأني نزلت تحت وما شفتها" — the
Manage tab's bulk-action toolbar (added in #332) was not
visible after scrolling down through the meetings list.
- Adds a `headingRef` + `ResizeObserver` effect in
`ManageSection` that publishes the live height of
`em-manage-heading-bar` as a `--em-manage-heading-h` CSS
var on the `[data-em-root]` element. Mirrors the Schedule
tab's existing pattern at L2155 for `--em-heading-h`.
Distinct var name avoids collision when switching tabs;
cleared on unmount so the Schedule tab doesn't inherit a
stale offset.
- Updates `em-manage-bulk-toolbar` to `position: sticky`
with `top: calc(var(--em-header-h) + var(--em-manage-heading-h))`,
matching horizontal bleed (`-mx-3 sm:-mx-6 px-3 sm:px-6`)
and `#f4f6fb` page-bg of the heading bar so the two visually
stack as a single header unit. Drops the rounded card style,
uses a bottom-border + drop-shadow for the seam.
- Adds `print:hidden` for parity with the Schedule
counterpart so the toolbar doesn't leak into PDF exports.
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Pre-existing
api-server / test workflow failures are unrelated to this
change. No behavior change to the Schedule tab.
Brings the Schedule tab's in-day search (#329) and bulk
delete/duplicate-to-date (#330) into the Manage tab of
executive-meetings.tsx (`ManageSection`).
- Adds `searchQuery` state + search input in the manage heading
bar; reuses module-level `normalizeForSearch` /
`meetingMatchesSearch` helpers and the same "reset on date
change" semantics as the Schedule tab.
- Adds `selectedMeetingIds` Set + tri-state "select all
visible" header checkbox + per-row checkbox column. Action
handlers always intersect the selection set with the visible
(filtered) rows, matching Schedule's safety pattern.
- Adds bulk-delete (with confirm + aggregated toast via
`Promise.allSettled`) and bulk-duplicate-to-date (separate
Dialog mirroring the Schedule one).
- Bumps loading/empty `colSpan` from 4 → 5 for the new
checkbox column.
- Reuses existing `executiveMeetings.schedule.*` locale keys
(already present in both ar.json + en.json from #329/#330);
no new strings were needed.
- New testids: `em-manage-search`, `em-manage-search-clear`,
`em-manage-select-all`, `em-manage-row-select-{id}`,
`em-manage-bulk-toolbar`, `em-manage-bulk-selection-count`,
`em-manage-bulk-clear-selection`, `em-manage-bulk-duplicate`,
`em-manage-bulk-duplicate-{date,confirm}`,
`em-manage-bulk-delete`.
Out of scope per plan (deferred): undo for bulk-delete on
Manage, cross-date "select all on this date".
`pnpm -C artifacts/tx-os exec tsc --noEmit` clean. Architect
review passed. Pre-existing api-server / test workflow
failures are unrelated to this change.
Lets users find a meeting in the day's schedule by typing part of its
title, an attendee name, or other text — instead of scrolling.
artifacts/tx-os/src/pages/executive-meetings.tsx
- Added two top-level helpers near translateSaveError:
- normalizeForSearch(input): strips HTML tags + entities, drops
Arabic diacritics + tatweel, folds common letter variants
(آأإٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي), lowercases.
- meetingMatchesSearch(meeting, needle): tests title (AR/EN),
attendee names + titles, location, notes, and merge text via
normalized substring containment.
- ScheduleSection: added searchQuery state, useEffect that resets it
whenever the active `date` changes, normalizedSearch memo, and a
filteredMeetings memo derived from orderedMeetings.
- Switched the row render path to filteredMeetings: SortableContext
items, the .map render, and the select-all overlay's "anything to
select" check now read from the filtered list. The
setAllMeetingsSelected and allSelectedState helpers also operate
on filteredMeetings so the header checkbox + select-all reflect
what's actually visible.
- Added a new "no matches" empty-state row that appears only when
the day has meetings but the active search filters them all out
(the original "no meetings today" row is preserved for empty days).
- New search Input + clear-X button placed in the schedule toolbar,
next to the existing date picker. Hidden in print so PDFs export
the full schedule.
- MeetingRow gained an optional `displayNumber` prop. The "#" cell
now renders displayNumber when provided, falling back to the
server's dailyNumber. The schedule passes idx+1, so visible rows
always show 1, 2, 3… without gaps when the list is filtered.
artifacts/tx-os/src/locales/ar.json + en.json
- New keys under executiveMeetings.schedule: searchPlaceholder,
searchAria, searchClear, searchNoMatches.
- Bulk delete now intersects the selection with filteredMeetings at
action time. The selection set is intentionally NOT auto-pruned
when the search narrows (so clearing the search restores the
user's earlier picks), but destructive actions only ever target
rows the user can currently see.
- Print/PDF export ignores the active search. An `isPrinting` flag
driven by beforeprint/afterprint + a `print` media-query listener
swaps the table to the unfiltered orderedMeetings while printing,
so generated PDFs always contain the full day's schedule even
when the user has typed into the search box.
Out of scope: cross-day search, prev/next-day chevron buttons,
search history, fuzzy matching.
Verified: `tsc --noEmit` clean.
Lets users find a meeting in the day's schedule by typing part of its
title, an attendee name, or other text — instead of scrolling.
artifacts/tx-os/src/pages/executive-meetings.tsx
- Added two top-level helpers near translateSaveError:
- normalizeForSearch(input): strips HTML tags + entities, drops
Arabic diacritics + tatweel, folds common letter variants
(آأإٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي), lowercases.
- meetingMatchesSearch(meeting, needle): tests title (AR/EN),
attendee names + titles, location, notes, and merge text via
normalized substring containment.
- ScheduleSection: added searchQuery state, useEffect that resets it
whenever the active `date` changes, normalizedSearch memo, and a
filteredMeetings memo derived from orderedMeetings.
- Switched the row render path to filteredMeetings: SortableContext
items, the .map render, and the select-all overlay's "anything to
select" check now read from the filtered list. The
setAllMeetingsSelected and allSelectedState helpers also operate
on filteredMeetings so the header checkbox + select-all reflect
what's actually visible.
- Added a new "no matches" empty-state row that appears only when
the day has meetings but the active search filters them all out
(the original "no meetings today" row is preserved for empty days).
- New search Input + clear-X button placed in the schedule toolbar,
next to the existing date picker. Hidden in print so PDFs export
the full schedule.
- MeetingRow gained an optional `displayNumber` prop. The "#" cell
now renders displayNumber when provided, falling back to the
server's dailyNumber. The schedule passes idx+1, so visible rows
always show 1, 2, 3… without gaps when the list is filtered.
artifacts/tx-os/src/locales/ar.json + en.json
- New keys under executiveMeetings.schedule: searchPlaceholder,
searchAria, searchClear, searchNoMatches.
- Bulk delete now intersects the selection with filteredMeetings at
action time. The selection set is intentionally NOT auto-pruned
when the search narrows (so clearing the search restores the
user's earlier picks), but destructive actions only ever target
rows the user can currently see.
Out of scope: cross-day search, prev/next-day chevron buttons,
search history, fuzzy matching.
Verified: `tsc --noEmit` clean.
Two small UX fixes for the Add/Edit Meeting save toast.
artifacts/tx-os/src/pages/executive-meetings.tsx
- Added a `translateSaveError(rawMsg, t)` helper near the other
top-level helpers (~line 425). It substring-matches the API's
Zod-generated message "startTime must be <= endTime" and returns
the translated copy from
`executiveMeetings.manage.errors.endTimeBeforeStart`. Anything
unknown falls through to the raw server message so unexpected
errors aren't silently hidden.
- The save catch block now passes the message through this helper
before showing the toast. Title still uses
`errors.saveFailed`.
artifacts/tx-os/src/locales/ar.json + en.json
- New translation key `executiveMeetings.manage.errors.endTimeBeforeStart`:
- ar: "وقت الانتهاء يجب أن يكون بعد وقت البدء"
- en: "End time must be after start time"
artifacts/tx-os/src/components/ui/toast.tsx
- ToastViewport: replaced the mobile-top + desktop-bottom-right
positioning with a single bottom-center placement on every
viewport (`fixed bottom-0 left-1/2 -translate-x-1/2`). Width
is still capped at `md:max-w-[420px]`. The flex direction is
`flex-col` (no longer `flex-col-reverse`) since there is now
only one slide-in direction.
- toastVariants: dropped `data-[state=open]:slide-in-from-top-full`
so toasts slide in from the bottom on every breakpoint, matching
the new viewport position.
Out of scope (untouched): API validation, other dialogs/pages,
preventive client-side validation for invalid time ranges.
After #322 trimmed the dialog, the Save/Cancel footer slid below
the visible 90vh sheet on phones because the whole DialogContent
was a single overflow-y-auto block.
Changes in artifacts/tx-os/src/pages/executive-meetings.tsx:
MeetingFormDialog
- Rebuilt DialogContent as a flex column: shrink-0 header,
flex-1 min-h-0 overflow-y-auto middle (the only scroller),
shrink-0 footer. Save/Cancel now stay pinned at the bottom
even after many attendees are added.
- Reduced mobile padding/gaps (`p-4 sm:p-6`,
`gap-3 sm:gap-4`, grid `gap-2 sm:gap-3`). Desktop spacing
unchanged.
- Switched the field grid from `grid-cols-1 sm:grid-cols-2`
to a flat `grid-cols-2` so the two short fields (التاريخ,
الرقم اليومي) share a row even on mobile. Title spans both
columns; time pickers span both on mobile and one each on
sm+ via the new mobileFull variant.
- Inner scroll wrapper uses negative margins + matching padding
so scroll content can extend edge-to-edge without losing the
dialog's gutter.
FormRow helper
- Broadened `full` from `sm:col-span-2` to `col-span-2`
(works in both 1-col and 2-col grids; only callers using
`full` are in this file).
- Added `mobileFull` prop → `col-span-2 sm:col-span-1` for
the time pickers, so AM/PM controls don't get squeezed on
phones.
Out of scope (untouched): validation, save logic, attendee
model, removed-field defaults from #322, duplicate dialog,
audit, schedule view, translations.
Refactor executive meetings form layout in `executive-meetings.tsx` to use a responsive grid, ensuring the Arabic title spans full width and other fields stack appropriately on smaller screens. Update Arabic and English locale files (`ar.json`, `en.json`) to change the Arabic title field label from "العنوان (عربي)" to "العنوان".
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 57375b7d-57d9-4fe3-a745-00a2cad59df8
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/2VzZgdn
Replit-Helium-Checkpoint-Created: true
Trim the إضافة اجتماع / تعديل اجتماع dialog and attendee rows in
artifacts/tx-os/src/pages/executive-meetings.tsx down to the fields
the user actually fills in.
MeetingFormDialog
- Removed FormRows: titleEn, location, meetingUrl, platform, status,
isHighlighted, notes.
- Kept titleAr, meetingDate, dailyNumber, startTime, endTime, and
the attendees section.
- Save validation now only requires titleAr.
- titleEn is always mirrored from titleAr on save. The English input
is gone, so there is no way to keep an independent English title;
always syncing avoids leaving a stale server-side titleEn after the
Arabic title is edited. The API still receives a non-empty titleEn
so POST validation (min length 1) is satisfied.
- MeetingFormState, emptyMeetingForm, and openEdit are unchanged so
hidden fields (location, notes, platform, status, isHighlighted,
meetingUrl) round-trip through the save body and existing data is
preserved on edit.
SortableAttendeeRow
- Removed the parenthesized title Input and the
internal/virtual/external Select.
- Dropped the now-unused onChangeTitle / onChangeAttendanceType
props from the component signature and the call site.
- Existing attendees keep their stored title and attendanceType;
new attendees added via addAttendee continue to default to
title=null and attendanceType="internal" so the schedule's
grouping keeps working.
Cleanup
- Removed the now-unused Textarea import (only the notes field
used it). Switch is still imported because it is used elsewhere
in the file.
Code review found two issues that were fixed before completion:
- Mirroring made deterministic (always sync) instead of
blank-only fallback.
- Orphaned Textarea import removed.
No DB schema, API, schedule view, Manage list, audit, notifications,
or translation changes.
Trim the إضافة اجتماع / تعديل اجتماع dialog and attendee rows in
artifacts/tx-os/src/pages/executive-meetings.tsx down to the fields
the user actually fills in.
MeetingFormDialog
- Removed FormRows: titleEn, location, meetingUrl, platform, status,
isHighlighted, notes.
- Kept titleAr, meetingDate, dailyNumber, startTime, endTime, and
the attendees section.
- Save validation now only requires titleAr.
- titleEn is always mirrored from titleAr on save. The English input
is gone, so there is no way to keep an independent English title;
always syncing avoids leaving a stale server-side titleEn after the
Arabic title is edited. The API still receives a non-empty titleEn
so POST validation (min length 1) is satisfied.
- MeetingFormState, emptyMeetingForm, and openEdit are unchanged so
hidden fields (location, notes, platform, status, isHighlighted,
meetingUrl) round-trip through the save body and existing data is
preserved on edit.
SortableAttendeeRow
- Removed the parenthesized title Input and the
internal/virtual/external Select.
- Dropped the now-unused onChangeTitle / onChangeAttendanceType
props from the component signature and the call site.
- Existing attendees keep their stored title and attendanceType;
new attendees added via addAttendee continue to default to
title=null and attendanceType="internal" so the schedule's
grouping keeps working.
Cleanup
- Removed the now-unused Textarea import (only the notes field
used it). Switch is still imported because it is used elsewhere
in the file.
Code review found two issues that were fixed before completion:
- Mirroring made deterministic (always sync) instead of
blank-only fallback.
- Orphaned Textarea import removed.
No DB schema, API, schedule view, Manage list, audit, notifications,
or translation changes.
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.
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.