- 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.
- 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.
Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.
Four review rounds were addressed in this commit:
Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
on note_recipients.note_id and note_replies.note_id so recipient
threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot.
Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.
Round 3:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
picker for owner replies on multi-recipient notes.
Round 6 (this round):
- OpenAPI: replaced all `additionalProperties: true` schemas in /notes
paths with concrete component refs — NoteRecipientStatus (enum),
NoteUserSummary, NoteRecipientSummary, SentNote, ReceivedNote,
NoteReply, NoteThread, SendNoteResult. Re-ran orval; api-zod and
api-client-react regenerated cleanly.
- Trimmed narrative comments in artifacts/api-server/src/routes/notes.ts
(handleNoteDetail, /sent, /received, /send, /read, /reply).
- Schema FK + enum policy: kept noteId as plain integer (no FK) on
note_recipients/note_replies — this is required so recipient
snapshots survive sender deletion. Status remains a varchar but is
constrained at the API boundary by the new NoteRecipientStatus enum
schema and TS string-literal union on both server and client.
- Migrations: this monorepo uses `drizzle-kit push` exclusively
(lib/db has no migrations directory; drizzle.config.ts is push-only;
package.json defines only push/push-force scripts). No migration
files committed by design.
Round 5: admin authorization fix
- handleNoteDetail: admin bypass is now evaluated BEFORE the
"non-participant 403" branch. When the sender has deleted the live
note row but recipient snapshots remain, an admin GET /notes/:id now
returns 200 with thread payload assembled from a fallback recipient
snapshot (instead of 403).
- New regression test: "admin can read a note whose sender deleted
their copy (recipient snapshot remains)".
Round 4:
- Added GET /notes/my as an explicit alias of GET /notes (shared
handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
SentNoteOut type derived from the query result.
- Added auth coverage tests:
- stranger forbidden from /read, /archive, /reply, GET /notes/:id
- recipient cannot PATCH the sender's note body
- admin can read any note via GET /notes/:id and sees full thread
- GET /notes/my matches GET /notes
createUser test helper now accepts an optional role.
Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.
Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
all pass. tx-os typecheck is clean. Architect re-review: PASS.
Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.
Four review rounds were addressed in this commit:
Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
on note_recipients.note_id and note_replies.note_id so recipient
threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot.
Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.
Round 3:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
picker for owner replies on multi-recipient notes.
Round 5 (this round): admin authorization fix
- handleNoteDetail: admin bypass is now evaluated BEFORE the
"non-participant 403" branch. When the sender has deleted the live
note row but recipient snapshots remain, an admin GET /notes/:id now
returns 200 with thread payload assembled from a fallback recipient
snapshot (instead of 403).
- New regression test: "admin can read a note whose sender deleted
their copy (recipient snapshot remains)".
Round 4:
- Added GET /notes/my as an explicit alias of GET /notes (shared
handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
SentNoteOut type derived from the query result.
- Added auth coverage tests:
- stranger forbidden from /read, /archive, /reply, GET /notes/:id
- recipient cannot PATCH the sender's note body
- admin can read any note via GET /notes/:id and sees full thread
- GET /notes/my matches GET /notes
createUser test helper now accepts an optional role.
Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.
Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
all pass. tx-os typecheck is clean. Architect re-review: PASS.
Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.
Four review rounds were addressed in this commit:
Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
on note_recipients.note_id and note_replies.note_id so recipient
threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot.
Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.
Round 3:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
picker for owner replies on multi-recipient notes.
Round 4 (this round):
- Added GET /notes/my as an explicit alias of GET /notes (shared
handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
SentNoteOut type derived from the query result.
- Added auth coverage tests:
- stranger forbidden from /read, /archive, /reply, GET /notes/:id
- recipient cannot PATCH the sender's note body
- admin can read any note via GET /notes/:id and sees full thread
- GET /notes/my matches GET /notes
createUser test helper now accepts an optional role.
Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.
Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
all pass. tx-os typecheck is clean. Architect re-review: PASS.
Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.
Three review rounds were addressed in this commit:
Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
on note_recipients.note_id and note_replies.note_id so recipient
threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot for
recipients (sender/admin still see the live note).
Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too. Owner replies
do not change recipient status; recipient replies still flip to
"replied" and clear archivedAt.
- Added GET /notes/:id as an alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes and ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.
Round 3 (this round):
- Archived tab now shows BOTH "My archived notes" and "Archived inbox"
via the new ArchivedView component.
- Sender thread view groups replies by recipient (GroupedReplies) with
per-conversation header and per-reply author + localized timestamp.
Recipient view stays flat.
- Send dialog now requires explicit confirmation (AlertDialog) and
shows a success / failure toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys
(notes.toast.received|replied.*) added in en.json and ar.json.
- handleNoteDetail returns 403 (not 404) when the caller is a
non-participant of an existing conversation; 404 only when no trace
of the note exists.
- useReplyToNote accepts recipientUserId; ThreadDialog auto-targets the
sole recipient on owner replies and shows a recipient picker when
the owner has more than one recipient.
Tests: 7 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox.spec.mjs)
all pass. tx-os typecheck is clean. Architect re-review: PASS.
Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.
Two prior code-review rounds were addressed in this commit:
Round 1 (independence):
- Added immutable snapshot columns (title/content/color) on note_recipients.
- Dropped the FK from note_recipients.note_id and note_replies.note_id so
recipient threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot for
recipients (sender/admin still see the live note).
Round 2 (validation REJECT fixes):
- POST /notes/:id/reply: owner is now allowed to reply too. Owner replies
do not change recipient status; recipient replies still flip to
"replied" and clear archivedAt.
- Added GET /notes/:id as an alias of /notes/:id/thread (shared handler).
- Added OpenAPI ops for /notes/sent, /notes/received, /notes/{id},
/notes/{id}/send, /notes/{id}/read, /notes/{id}/archive,
/notes/{id}/reply, and ran orval codegen.
- use-notifications-socket.ts now shows bilingual toasts for note_received
and note_replied (suppressed during the socket warmup window).
- home.tsx renders an unread badge on the Notes app tile, fed by
useReceivedNotes(false) filtered to status === "unread"; refreshes
automatically on socket invalidation.
Tests: 7 backend tests in notes-share.test.mjs (independence,
archived-reply, owner-reply preserves recipient status, GET /notes/:id
alias, etc.) + the notes-inbox e2e all pass. tx-os typecheck is clean.
Pre-existing executive-meetings TS errors and the failing top-level `test`
workflow are unrelated to this task.
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an Inbox
with sender name, color, Read/Unread badge, and inline reply. Sender sees
Sent Notes with per-recipient status. Sender's and recipient's copies must
be INDEPENDENT, with proper backend access checks (admin sees everything),
realtime updates, and full i18n + RTL.
Initial implementation review FAILED because the recipient view still read
from the sender's notes table, so sender edits/deletes mutated recipient
copies. This commit completes the fix:
- Schema (lib/db/src/schema/notes.ts): added immutable snapshot columns
(title/content/color) on note_recipients; dropped the FK on
note_recipients.note_id and note_replies.note_id and made them plain
integers so recipient threads survive the sender deleting their note.
- Routes (artifacts/api-server/src/routes/notes.ts):
- /notes/received and /notes/:id/thread now serve the recipient snapshot
(sender/admin still see the live note).
- /notes/:id/reply derives the owner from note_recipients.senderUserId
so it works after sender deletion, clears archivedAt, and bumps
status to "replied".
- /notes/sent filters out null noteIds.
- Tests (artifacts/api-server/tests/notes-share.test.mjs): added
snapshot-independence test (sender edit + delete must not mutate
recipient copy; thread + reply still work after sender delete) and
archived-reply-clears-archivedAt test. All 5 backend tests pass; the
existing notes-inbox e2e still passes.
- Architect review: PASS (conditional only on the schema migration being
applied, which has been pushed via `pnpm --filter @workspace/db push`).
Pre-existing executive-meetings TS errors and the failing `test` workflow
are unrelated to this task.
Modify the order confirmation modal title in both Arabic and English locale files to a generic confirmation message, resolving a UI redundancy where the item name was displayed twice.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d2a9850b-3afa-47aa-b54b-73a5f14c5ac6
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UggHvbd
Replit-Helium-Checkpoint-Created: true
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
shared authorizeAndDeleteOrder() helper so single and bulk paths use
the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization to mirror GET /orders/incoming: a
receiver may only delete an unclaimed pending order or one they have
themselves claimed and is still active (received/preparing). Another
receiver's claimed order, and any terminal order, return 403 even at
the API layer. Code, comments and OpenAPI description now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — both
useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
pluralized confirmation copy. Single delete calls DELETE /orders/:id;
multi delete calls POST /orders/bulk-delete; partial-failure surfaces
via toast.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.
Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
delete pending/received/preparing; receivers cannot delete terminal
(completed/cancelled); bulk-delete with mixed authorized / unknown /
dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
bulk-delete two unclaimed orders shrinks the list by 2; per-card
trash on a claimed order deletes one. Both pass.
Pre-existing executive-meetings PDF/font test failures are unrelated.
Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
shared authorizeAndDeleteOrder() helper so single and bulk paths use
the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization to mirror GET /orders/incoming: a
receiver may only delete an unclaimed pending order or one they have
themselves claimed and is still active (received/preparing). Another
receiver's claimed order, and any terminal order, return 403 even at
the API layer. Code, comments and OpenAPI description now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — both
useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
pluralized confirmation copy. Single delete calls DELETE /orders/:id;
multi delete calls POST /orders/bulk-delete; partial-failure surfaces
via toast.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.
Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
delete pending/received/preparing; receivers cannot delete terminal
(completed/cancelled); bulk-delete with mixed authorized / unknown /
dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
bulk-delete two unclaimed orders shrinks the list by 2; per-card
trash on a claimed order deletes one. Both pass.
Pre-existing executive-meetings PDF/font test failures are unrelated.
Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
shared authorizeAndDeleteOrder() helper so single and bulk paths use
the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization to mirror GET /orders/incoming: a
receiver may only delete an unclaimed pending order or one they have
themselves claimed and is still active (received/preparing). Another
receiver's claimed order, and any terminal order, return 403 even at
the API layer. Code, comments and OpenAPI description now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — both
useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
pluralized confirmation copy. Single delete calls DELETE /orders/:id;
multi delete calls POST /orders/bulk-delete; partial-failure surfaces
via toast.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.
Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
delete pending/received/preparing; receivers cannot delete terminal
(completed/cancelled); bulk-delete with mixed authorized / unknown /
dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
bulk-delete two unclaimed orders shrinks the list by 2; per-card
trash on a claimed order deletes one. Both pass.
Pre-existing executive-meetings PDF/font test failures are unrelated.
Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
Backend (artifacts/api-server):
- Added hasReceivePermission() and refactored DELETE /orders/:id into a
shared authorizeAndDeleteOrder() helper so single and bulk paths use
the same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization: receivers may only delete orders
currently in the incoming queue (pending/received/preparing). Terminal
orders remain the owner's cleanup responsibility. Code, comments and
OpenAPI description now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas. Updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — both
useDeleteServiceOrder and useBulkDeleteServiceOrders are used by the UI.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Per-card checkbox visible at all times (RTL-safe leading edge).
- Per-card trash icon for single delete.
- Section-scoped Select-all (مين / unclaimed) with tri-state
all/some(indeterminate)/none and shadcn Checkbox.
- Sticky bottom bulk action bar shows selected count + Clear selection +
destructive Delete.
- Single AlertDialog used by both per-card and bulk paths, with
pluralized confirmation copy. Single delete calls DELETE /orders/:id;
multi delete calls POST /orders/bulk-delete; partial-failure surfaces
via toast.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization in both languages.
Tests:
- artifacts/api-server/tests/service-orders.test.mjs: receivers can
delete pending/received/preparing; receivers cannot delete terminal
(completed/cancelled); bulk-delete with mixed authorized / unknown /
dedup / unauthenticated cases.
- artifacts/tx-os/tests/order-incoming-delete.spec.mjs (new Playwright):
bulk-delete two unclaimed orders shrinks the list by 2; per-card
trash on a claimed order deletes one. Both pass.
Pre-existing executive-meetings PDF/font test failures are unrelated.
Follow-ups proposed: #398 (Undo for incoming-order deletes), #399
(safer notification handling in bulk-delete).
Backend (artifacts/api-server):
- Added hasReceivePermission() helper and refactored DELETE /orders/:id
into shared authorizeAndDeleteOrder() so single and bulk paths use the
same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization (per code review): receivers may only
delete pending/received/preparing orders; terminal (completed/cancelled)
orders remain the owner's cleanup responsibility. OpenAPI description
and the implementation now agree.
API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
BulkDeleteServiceOrdersResponse schemas; updated DELETE /orders/{id}
description. Regenerated react-query hooks via codegen — useBulkDelete
ServiceOrders is now available.
UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Selection mode toggle in the page header (RTL-safe).
- Per-card Checkbox plus a Select-all control.
- Sticky bottom action bar with destructive Delete and selected count.
- AlertDialog confirmation using pluralized i18n keys; toast surfaces
partial-failure results when some ids couldn't be deleted.
i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization.
Tests: Added two new tests in service-orders.test.mjs covering receiver
delete on incoming queue, receiver forbidden on terminal orders, and
bulk-delete with mixed authorized / unknown / dedup / unauthenticated
cases. Both pass. Pre-existing executive-meetings PDF/font test failures
are unrelated.
Regenerated artifacts/tx-os/public/service-images/saudi-coffee.png
with a sharper prompt that explicitly names a handleless Saudi
finjan (small flared white porcelain cup with a thin gold patterned
rim, no handle) and a clear warm brown coffee color for both the
pour and the liquid in the cup. Strengthened the negative prompt
against handled cups, mugs, amber/orange liquid, and tea so the new
image no longer reads as "tea in a teacup".
Same path as before, so no DB change is needed — the existing
services.image_url ("service-images/saudi-coffee.png") on row id=2
keeps pointing at the new file.
No code, schema, locale, or test changes. Pre-existing `test`
workflow failure is unrelated.
Regenerated artifacts/tx-os/public/service-images/saudi-coffee.png
with a sharper prompt that explicitly names a handleless Saudi
finjan (small flared white porcelain cup with a thin gold patterned
rim, no handle) and a clear warm brown coffee color for both the
pour and the liquid in the cup. Strengthened the negative prompt
against handled cups, mugs, amber/orange liquid, and tea so the new
image no longer reads as "tea in a teacup".
Same path as before, so no DB change is needed — the existing
services.image_url ("service-images/saudi-coffee.png") on row id=2
keeps pointing at the new file.
No code, schema, locale, or test changes. Pre-existing `test`
workflow failure is unrelated.
Two small follow-ups on Task #394 polish:
1. Regenerated artifacts/tx-os/public/service-images/saudi-coffee.png
with a tighter prompt — just the brass dallah pouring brown
coffee into a small white finjan, soft beige background, no
dates/cardamom/garnish. Same path so no DB change needed.
2. Shrunk the "إضافة ملاحظات / Add notes" toggle in
OrderServiceModal: replaced the chunky outline Button with a
small ghost text-link style (text-xs, muted, pencil icon size 12,
underline-on-hover). It now reads as a secondary affordance and
sits below the service-preview row, well clear of the dialog's
absolute close (X) at right-4 top-4 — no overlap in either AR or
EN.
Behavior unchanged: data-testid="order-notes-toggle" preserved,
tapping still expands and focuses the textarea, hide link still
collapses without losing typed text.
Verification:
- pnpm --filter @workspace/tx-os typecheck → passes.
- The pre-existing `test` workflow failure is unrelated to this
change.
Three small UX fixes on the Services screen:
1. Renamed service id=2 to "قهوة سعودي" / "Saudi Coffee" and swapped
the thumbnail to a new clear brown image (small dallah pouring
into a finjan with cardamom + a date) saved at
public/service-images/saudi-coffee.png. The legacy
arabic-coffee.png is left in place so any cached references keep
loading.
2. Renamed service id=2450 Arabic name to "بلاك كوفي" (English
"Black Coffee" unchanged).
3. OrderServiceModal now opens with the notes textarea collapsed and
not mounted, so iPad/Safari no longer auto-pops the keyboard. A
small outline "إضافة ملاحظات / Add notes" button (with pencil
icon) expands it and focuses the textarea on demand. While
expanded, a small "إخفاء / Hide" link collapses it again without
clearing typed text. If initialNotes is non-empty when the dialog
opens, it starts expanded. Submission behavior unchanged.
Verified DB state after updates:
id=2 name_ar=قهوة سعودي name_en=Saudi Coffee image_url=service-images/saudi-coffee.png
id=2450 name_ar=بلاك كوفي name_en=Black Coffee image_url=service-images/black-coffee.png
Added i18n keys services.addNotes / services.hideNotes to ar.json
and en.json. Added data-testid="order-notes-toggle" and
data-testid="order-notes-textarea" for future tests.
`pnpm --filter @workspace/tx-os typecheck` passes. The pre-existing
`test` workflow failure is unrelated to this change.
Three small UX fixes on the Services screen:
1. Renamed service id=2 to "قهوة سعودي" / "Saudi Coffee" and swapped
the thumbnail to a new clear brown image (small dallah pouring
into a finjan with cardamom + a date) saved at
public/service-images/saudi-coffee.png. The legacy
arabic-coffee.png is left in place so any cached references keep
loading.
2. Renamed service id=2450 Arabic name to "بلاك كوفي" (English
"Black Coffee" unchanged).
3. OrderServiceModal now opens with the notes textarea collapsed and
not mounted, so iPad/Safari no longer auto-pops the keyboard. A
small outline "إضافة ملاحظات / Add notes" button (with pencil
icon) expands it and focuses the textarea on demand. While
expanded, a small "إخفاء / Hide" link collapses it again without
clearing typed text. If initialNotes is non-empty when the dialog
opens, it starts expanded. Submission behavior unchanged.
Verified DB state after updates:
id=2 name_ar=قهوة سعودي name_en=Saudi Coffee image_url=service-images/saudi-coffee.png
id=2450 name_ar=بلاك كوفي name_en=Black Coffee image_url=service-images/black-coffee.png
Added i18n keys services.addNotes / services.hideNotes to ar.json
and en.json. Added data-testid="order-notes-toggle" and
data-testid="order-notes-textarea" for future tests.
`pnpm --filter @workspace/tx-os typecheck` passes. The pre-existing
`test` workflow failure is unrelated to this change.
Three small UX fixes on the Services screen:
1. Renamed service id=2 to "قهوة سعودي" / "Saudi Coffee" and swapped
the thumbnail to a new clear brown image (small dallah pouring
into a finjan with cardamom + a date) saved at
public/service-images/saudi-coffee.png. The legacy
arabic-coffee.png is left in place so any cached references keep
loading.
2. Renamed service id=2450 Arabic name to "بلاك كوفي" (English
"Black Coffee" unchanged).
3. OrderServiceModal now opens with the notes textarea collapsed and
not mounted, so iPad/Safari no longer auto-pops the keyboard. A
small outline "إضافة ملاحظات / Add notes" button (with pencil
icon) expands it and focuses the textarea on demand. While
expanded, a small "إخفاء / Hide" link collapses it again without
clearing typed text. If initialNotes is non-empty when the dialog
opens, it starts expanded. Submission behavior unchanged.
Verified DB state after updates:
id=2 name_ar=قهوة سعودي name_en=Saudi Coffee image_url=service-images/saudi-coffee.png
id=2450 name_ar=بلاك كوفي name_en=Black Coffee image_url=service-images/black-coffee.png
Added i18n keys services.addNotes / services.hideNotes to ar.json
and en.json. Added data-testid="order-notes-toggle" and
data-testid="order-notes-textarea" for future tests.
`pnpm --filter @workspace/tx-os typecheck` passes. The pre-existing
`test` workflow failure is unrelated to this change.
Three small UX fixes on the Services screen:
1. Renamed service id=2 to "قهوة سعودي" / "Saudi Coffee" and swapped
the thumbnail to a new clear brown image (small dallah pouring
into a finjan with cardamom + a date) saved at
public/service-images/saudi-coffee.png. The legacy
arabic-coffee.png is left in place so any cached references keep
loading.
2. Renamed service id=2450 Arabic name to "بلاك كوفي" (English
"Black Coffee" unchanged).
3. OrderServiceModal now opens with the notes textarea collapsed and
not mounted, so iPad/Safari no longer auto-pops the keyboard. A
small outline "إضافة ملاحظات / Add notes" button (with pencil
icon) expands it and focuses the textarea on demand. While
expanded, a small "إخفاء / Hide" link collapses it again without
clearing typed text. If initialNotes is non-empty when the dialog
opens, it starts expanded. Submission behavior unchanged.
Verified DB state after updates:
id=2 name_ar=قهوة سعودي name_en=Saudi Coffee image_url=service-images/saudi-coffee.png
id=2450 name_ar=بلاك كوفي name_en=Black Coffee image_url=service-images/black-coffee.png
Added i18n keys services.addNotes / services.hideNotes to ar.json
and en.json. Added data-testid="order-notes-toggle" and
data-testid="order-notes-textarea" for future tests.
`pnpm --filter @workspace/tx-os typecheck` passes. The pre-existing
`test` workflow failure is unrelated to this change.
User reported a flood of chimes when opening Tx OS while several
notifications were pending. Two complementary fixes:
1. Player throttle bumped from 600 ms → 3000 ms in
artifacts/tx-os/src/lib/notification-sounds.ts. Bursts of real-time
events now produce a single chime instead of several overlapping ones.
2. Socket warmup gate added in
artifacts/tx-os/src/hooks/use-notifications-socket.ts:
- Track connectedAtRef (useRef<number>), set on effect mount and on
each socket "connect" event using performance.now().
- In the notification_created handler, after mute / per-channel
gates but before notificationPlayer.play(...), suppress
audio + vibration when within the 3 s warmup window.
- UI/badge invalidation still runs — only sound is silenced during
the warmup, so the user sees notifications appear without an
alarm-like welcome.
All existing gates preserved (global mute, per-channel sound/vibration,
autoplay-blocked toast hint, per-meeting playedRef dedupe).
No deviations. tx-os typecheck passes.
Moved the live HeaderClock from the actions cluster (next to the Export
PDF button) to the geometric center of the top header row.
Implementation:
- Made the header row container `relative`.
- Removed `<HeaderClock />` from the trailing actions `<div>`.
- Added an absolutely-positioned overlay (`absolute inset-x-0 top-0
bottom-0 flex items-center justify-center pointer-events-none`)
containing the clock, with the inner wrapper restoring
`pointer-events-auto`. This keeps the clock visually centered
regardless of whether the title or actions on either side are wider.
- Kept all #391 behaviors: `hidden sm:flex`, `print:hidden`, 1s tick,
ar/en locale formatting, palette text-[#0B1E3F], tabular-nums,
data-testid="em-header-clock".
No deviation from plan. tx-os typecheck passes.
Added a `HeaderClock` component to artifacts/tx-os/src/pages/executive-meetings.tsx
that renders next to the "Export PDF" button in the sticky page header.
- Updates every 1s via setInterval (cleared on unmount)
- Locale-aware formatting via Intl.DateTimeFormat (ar / en) with hour12
- Hidden on <sm screens (`hidden sm:flex`) and on print
- Uses existing palette (text-[#0B1E3F]) + Clock lucide icon
- tabular-nums to prevent jitter as digits change
- data-testid="em-header-clock" for future tests
No deviations from plan. tx-os typecheck passes.
Removed the `document.visibilityState === "visible"` early-return from
two playback paths so notification sounds fire even when the Tx OS tab
is the active foreground tab:
- `use-notifications-socket.ts` — order + meeting socket events.
- `upcoming-meeting-alert.tsx` — 5-min upcoming-meeting alert chime.
All other gates remain unchanged: global mute, per-channel sound and
vibration toggles, the ~600ms throttle in the audio player, and the
per-meeting `playedRef` dedupe by `meetingId`.
Verified `pnpm --filter @workspace/tx-os typecheck` passes.
DB
- Added user pref columns; vibration is per-channel
(vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
sounds, vibration on, mute off. Volume uses the device system level —
no persisted volume field.
API
- PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial
update, integer guard for volume). buildAuthUser exposes all new fields.
- OpenAPI spec updated; orval client + zod regenerated.
Frontend
- Static sound library: 8 short WAV assets under public/sounds/ + manifest
(notification-sound-manifest.ts) mapping id -> labelKey -> URL.
- HTMLAudio-based player (notification-sounds.ts) with throttle, unlock,
cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture.
- Settings popover: global mute + slot tabs (orders/meetings) with
per-channel sound + per-channel vibration + per-channel toggle, sound list
with one-tap previews. Concurrency-safe optimistic updates (monotonic seq
counter). RTL-correct toggle knob transforms.
- QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with
confirmation toast.
- useAudioUnlock: unlocks audio on first interaction.
- useAutoplayHint: toasts a localized "click anywhere to enable" hint when
the player reports a blocked play attempt.
- Socket hook plays the right per-channel sound + vibration on
notification_created (orders, executive_meeting) when the tab is hidden.
- UpcomingMeetingAlert: plays meeting reminder once per new eligible
meeting (deduped by meetingId, survives 30s polling refetches). Skipped
when the tab is currently visible — sound only fires when backgrounded.
- AR/EN translations added.
Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
DB
- Added 7 user pref columns; vibration is now per-channel
(vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
sounds, vibration on, mute off, volume 70.
API
- PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial
update, integer guard for volume). buildAuthUser exposes all new fields.
- OpenAPI spec updated; orval client + zod regenerated.
Frontend
- Static sound library: 8 short WAV assets under public/sounds/ + manifest
(notification-sound-manifest.ts) mapping id -> labelKey -> URL.
- HTMLAudio-based player (notification-sounds.ts) with throttle, unlock,
cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture.
- Settings popover: global mute, volume, slot tabs (orders/meetings) with
per-channel sound + per-channel vibration + per-channel toggle, sound list
with one-tap previews. Concurrency-safe optimistic updates (monotonic seq
counter). RTL-correct toggle knob transforms.
- QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with
confirmation toast.
- useAudioUnlock: unlocks audio on first interaction.
- useAutoplayHint: toasts a localized "click anywhere to enable" hint when
the player reports a blocked play attempt.
- Socket hook plays the right per-channel sound + vibration on
notification_created (orders, executive_meeting) when the tab is hidden.
- UpcomingMeetingAlert: plays meeting reminder once per new eligible
meeting (deduped by meetingId, survives 30s polling refetches).
- AR/EN translations added.
Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
- DB: 7 new user prefs (sound per slot, per-type toggles, vibration, mute, volume).
- API: PATCH /auth/me/notification-preferences with whitelisted partial update,
integer guard for volume, and hydrated AuthUser response. AuthUser now exposes
the 7 new fields.
- Frontend: Web Audio synth library (8 sounds), settings popover with global
mute/vibration/volume/per-type toggles, slot tabs, sound preview buttons,
shift+click on bell for quick mute. Concurrency-safe optimistic updates with
monotonic seq counter. RTL-correct toggle knob transforms.
- Socket hook plays sound on notification_created (orders/meetings only) when
tab is hidden, respecting global mute and per-type toggles.
- Audio unlock hook mounted globally to satisfy autoplay restrictions.
- AR/EN translations added.
Pre-existing TS errors in executive-meetings.ts (font_settings) and pre-existing
test failures in test workflow are unrelated to this task.
Update localization files and executive-meetings.tsx to implement sequential bulk deletion with progress tracking and cancellation, mirroring the UX of the bulk duplicate feature.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3a577026-2b09-48ef-a405-e40e0b782577
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UggHvbd
Replit-Helium-Checkpoint-Created: true
Integrates a progress bar and cancel functionality into the bulk duplicate process for executive meetings, migrating from `Promise.allSettled` to a sequential loop to prevent race conditions and updating UI components and localization files.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7c55644f-4e59-4e78-a127-9aec1a90ebaf
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UggHvbd
Replit-Helium-Checkpoint-Created: true
Add zoom and fullscreen controls to the executive meeting schedule page, including zoom in/out buttons, a fullscreen toggle, and Escape key handling for exiting fullscreen. The heading height calculation has been removed and hardcoded to 0px.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 608173c1-6210-41cf-be7d-3a913963059f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jwG0a8d
Replit-Helium-Checkpoint-Created: true