Commit Graph

715 Commits

Author SHA1 Message Date
riyadhafraa 25f624f8d8 Task #429: folders-as-files browser at "all" notes view
- Notes reverted to original sticky-note cards (Section + NoteCard) at all
  views, removing the file-manager-style note rendering from task #427.
- Folders now render as a file-manager browser at the top of the "All notes"
  view: big black folder icon, name, relative time, item-count badge, and a
  "…" kebab with rename/delete. Grid and list layouts honor the existing
  view-toggle, plus a "{{count}} folders" footer.
- Added `mode: "rail" | "chips-only"` prop to FoldersRail. At "all" view the
  rail switches to chips-only (All / Unfiled / + New folder) so folder rows
  live exclusively in the new browser and test ids never collide.
- View-toggle is now hidden when not at "all" (folders-only control).
- New FoldersBrowser/FolderItem reuse useUpdateFolder/useDeleteFolder, set
  data-folder-drop for the touch DnD pipeline, and stop click propagation
  inside the dropdown so rename/delete don't navigate the card.
- Added locale keys: notes.folders.actions/renameFailed/itemCount(One).
- Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint
  helpers (~650 lines).
- Verified: tsc --noEmit clean; notes-folders, notes-card-touch-actions,
  notes-checklist, notes-delete-confirm Playwright suites all green.
2026-05-07 08:14:23 +00:00
riyadhafraa 097baddddb Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: eaceab9d-0e1b-4aa6-ab56-3e69031fcc59
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/5kzMIgG
Replit-Helium-Checkpoint-Created: true
2026-05-07 07:59:36 +00:00
riyadhafraa 1676a54ff8 Add file manager view to notes section with grid and list options
Adds new translation keys for UI elements and implements a view mode toggle (grid/list) for the notes page, persisting the selection in local storage.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 88b40c3d-8ff3-492f-891b-c2b0853ab41d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/5kzMIgG
Replit-Helium-Checkpoint-Created: true
2026-05-07 07:52:50 +00:00
riyadhafraa fc14ca4c01 Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup`
rendered a `fixed inset-0 z-[110]` full-viewport wrapper. Stacking
that layer on top of the upcoming-meeting alert plus a Radix toast
intermittently caused the next finger tap to be swallowed — the user
saw the popup "hang". A simultaneous re-render storm from
synchronous query invalidations on multi-event socket fanouts made
it worse on slower hardware.

Fixes:
- Drop the full-viewport wrapper: render the popup card directly
  as a sized `fixed` element. No more invisible viewport-sized
  layer between touch and the rest of the UI.
- Suppress the redundant note/reply toast when the floating
  popup actually accepts the event. The popup IS the surface;
  doubling up just stacks one more floating layer.
- Remove `autoFocus` on the Reply button — focus-steal was
  contributing to the perceived hang.
- Coalesce `react-query` `invalidateQueries` calls in the
  notifications socket: collect into a Set and flush once per
  animation frame so a burst of socket events triggers a single
  refetch pass instead of N synchronous re-renders. Cleaned up
  on socket disconnect.

Tests (artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs):
- "recipient on a touch device can tap Reply on the floating
  note popup" — verifies the popup is no longer viewport-sized,
  the duplicate toast is suppressed, and a real `locator.tap()`
  succeeds.
- "popup + meeting alert simultaneously: tap on popup still
  works" — the user's exact scenario: seeds an imminent meeting
  so the alert appears, triggers a real cross-user note
  delivery so both float simultaneously, then taps Reply on
  the popup and confirms it dismisses while the alert remains.

Both tests + tsc --noEmit clean.

Files:
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
- artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new)
2026-05-06 11:54:13 +00:00
riyadhafraa e283a83162 Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup`
rendered a `fixed inset-0 z-[110]` full-viewport wrapper with
`pointer-events-none`. Stacking that layer on top of the upcoming
meeting alert plus a Radix toast intermittently caused the next
finger tap to be swallowed — the user saw the popup "hang".

Fixes:
- Drop the full-viewport wrapper: render the popup card directly
  as a sized `fixed` element. No more invisible viewport-sized
  layer between touch and the rest of the UI.
- Suppress the redundant note/reply toast when the floating
  popup actually accepts the event. The popup IS the surface;
  doubling up just stacks one more floating layer.
- Remove `autoFocus` on the Reply button. A new popup arriving
  while the user is mid-tap would steal focus, contributing to
  the perceived hang.

Tests:
- New `notes-popup-touch-tap.spec.mjs`: emulates a touch viewport,
  sends a real cross-user note, verifies popup bounding box is
  card-sized (not viewport-sized), the redundant toast is
  suppressed, and a `locator.tap()` on Reply dismisses the
  popup and navigates.
- `tsc --noEmit` clean.

Files:
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
- artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new)
2026-05-06 11:43:23 +00:00
riyadhafraa 9937cb1b08 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 17d62187-995b-45bd-98b9-6f10e3ed6096
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/XE1wdRt
Replit-Helium-Checkpoint-Created: true
2026-05-06 11:25:48 +00:00
riyadhafraa 32db433581 notes: show note-card actions on touch devices (task #425)
Original task: on phones and iPad the per-card action row (color,
label, send, archive, delete) was hidden by `opacity-0
group-hover:opacity-100`. Touch devices have no hover, so users
couldn't send a freshly-created note (or change its color, label,
archive it).

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - Add Tailwind v4 arbitrary variant `[@media(hover:none)]:opacity-100`
    to the per-card action row so it stays always-visible on touch
    devices while desktops keep the existing hover-reveal behaviour.
  - Apply the same variant to the unpinned pin button (which uses the
    identical hover-only treatment).

Tests
- artifacts/tx-os/tests/notes-card-touch-actions.spec.mjs (new)
  - Uses `test.use({ hasTouch: true, isMobile: true, viewport })` on
    chromium (webkit isn't installed) so the page reports
    `(hover: none)`. Sanity-checks that via matchMedia, then creates a
    note via the composer (tap-outside auto-save) and asserts both
    that the Send button is visible and that the action row's
    computed opacity is exactly 1 — Playwright's `toBeVisible` alone
    doesn't catch opacity-0.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm
  notes-card-touch-actions: 3 passed (touch test + the two
  delete-confirm specs from #423 to confirm no regression).
- Architect code review: no severe issues. Desktop hover-reveal is
  preserved because `(hover: none)` doesn't match on pointer devices.

Out of scope (per task plan): the trash button inside the
manage-labels dialog (different surface, dialog-only).
2026-05-06 10:39:59 +00:00
riyadhafraa 2027ee2545 notes: replace native confirm() with styled AlertDialog (task #423)
Original task: deleting a note (and a label from the manage-labels menu)
showed the browser's native confirm() with the raw Repl hostname,
clashing with the rest of the app. Switch both call sites to the
project's existing AlertDialog component, matching the pattern used in
FoldersRail and SendNoteDialog.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - NoteCard: add `confirmDeleteOpen` state; render AlertDialog as a
    Fragment sibling of the clickable card (not a child) so React's
    portal event bubbling cannot trigger the card's onEdit handler when
    the user interacts with the dialog. Also stopPropagation on the
    trash button itself.
  - LabelsDialog: add `pendingDeleteLabel` state and a sibling
    AlertDialog inside the Dialog root. Both dialogs use existing
    locale keys (notes.deleteConfirm, notes.deleteLabelConfirm,
    notes.delete, common.cancel) — no new strings needed.
  - Add data-testids: note-card-delete-{id}, note-delete-dialog-{id},
    note-delete-confirm-{id}, label-delete-{id},
    label-delete-dialog-{id}, label-delete-confirm-{id},
    notes-manage-labels-open.

Tests
- artifacts/tx-os/tests/notes-delete-confirm.spec.mjs (new)
  - Note delete: cancel keeps the card, confirm fires DELETE and
    removes the card. Deliberately does NOT register a `dialog` event
    handler — a native confirm would hang the test.
  - Label delete: open manage-labels, create a label via the UI, click
    trash, confirm, assert DELETE /api/note-labels/{id}.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm:
  2 passed.
- Architect code review surfaced a real bubble-through risk on the
  card's onClick={onEdit}; fixed by lifting the AlertDialog to a
  Fragment sibling.

Out of scope (not changed): native confirm()/alert() outside the Notes
page. (executive-meetings.tsx already uses an in-app `confirm({...})`
modal, not the native browser dialog.)
2026-05-06 10:30:28 +00:00
riyadhafraa e82a2b83c2 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fb93ed69-be42-4dc9-a584-41c32f35d212
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/XE1wdRt
Replit-Helium-Checkpoint-Created: true
2026-05-06 10:19:47 +00:00
riyadhafraa 1ffb470e8f notes: add per-note checklist (to-do list) option
Task #420 — answers the user request "وين خيار اضيف list to do?".

Schema (lib/db/src/schema/notes.ts):
- notes + note_recipients gain `kind` (varchar(16) default 'text')
  and `items` (jsonb<ChecklistItem[]> nullable). Snapshot copy on
  note_recipients keeps delivered checklists immutable across sender
  edits/deletes.
- Drizzle push applied; lib/db .d.ts rebuilt.

API (artifacts/api-server/src/routes/notes.ts):
- ChecklistItem type + bounded parser (≤200 items, id ≤64, text ≤500).
- parseNoteInput normalizes items↔kind on create and on PATCH that
  carries `kind`; PATCH handler additionally coerces items-only
  patches against the note's existing kind so a text note can never
  end up with checklist items (and vice versa).
- POST/PATCH/send/loadRecipientsForNote/received/thread responses and
  the realtime `note_received` payload all carry kind+items, with the
  thread response falling back to the recipient snapshot.

Client (artifacts/tx-os/src/lib/notes-api.ts + pages/notes.tsx):
- Note/ReceivedNote/NoteThread/SentNoteRecipient extended with
  kind+items.
- New `ChecklistEditor`, `ChecklistView`, and `KindToggle` components.
- Composer and EditNoteDialog gain the to-do toggle (ListTodo icon)
  and switch between Textarea and ChecklistEditor; saves send
  kind+items, with empty-checklist auto-discard mirroring the existing
  empty-text behaviour.
- NoteCard, inbox list, sent list, and ThreadDialog body render the
  checklist (read-only on snapshots; owner cards can toggle done via
  PATCH with stopPropagation so the edit dialog doesn't open).

i18n: notes.checklist.{toggle,addItem,itemPlaceholder,emptyHint,
removeItem,progress} added to en.json + ar.json.

Tests: new artifacts/tx-os/tests/notes-checklist.spec.mjs covers
composer→persist→reload→toggle, items-only PATCH normalization on
both kinds, and checklist delivery to recipient snapshot. All 3 pass.

Architect review (evaluate_task) flagged one real issue: items-only
PATCH normalization. Fixed in the PATCH handler and locked in by the
new normalization test.

Pre-existing executive-meetings.ts tsc errors are unchanged and
unrelated to this task.
2026-05-06 10:12:52 +00:00
riyadhafraa d4e0dfc6e5 notes: persist chosen color (and labels) when composer auto-saves
When creating a new note, picking a color from the palette had no
effect — the note saved with the default white background. Editing an
existing note worked because that path lives in a Dialog with no
auto-save guard.

Root cause
The inline Composer in artifacts/tx-os/src/pages/notes.tsx attaches a
document-level mousedown listener that auto-saves whenever the click
falls outside its container ref. The color and label menus are built
on Radix Popover, which renders content in a portal outside the
composer's DOM tree. So clicking a swatch fired:
  1. document mousedown -> save() with the OLD color
  2. swatch click       -> setColor(new) (state already discarded)

Fix
The mousedown handler now ignores clicks whose target is inside any
Radix popper wrapper, Radix portal, or role="dialog" element. The
guard is narrow: legitimate outside clicks (page background, sidebar,
other notes) still trigger save() exactly as before. The same fix
covers the LabelMenu popover for free since it shares the abstraction.

Tests
- New tests/notes-composer-color.spec.mjs reproduces the original
  failure mode: open composer, type title, pick the "red" swatch from
  the popover, click outside, assert the POST /api/notes response has
  color === "red".
- Existing notes-folders + new spec run green together (3 passed in
  29.1s); tx-os tsc --noEmit clean.

Architect notes (followed inline / deferred)
- Suggested mirroring a labels-popover e2e — same code path is already
  exercised; can be added later if regressions surface.
- composedPath() hardening — current target.closest() works for all
  real Radix portals; not needed.
2026-05-06 09:59:10 +00:00
riyadhafraa e96d709bb1 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 70f56bc6-af8d-4b43-afd8-10d93fe8cfb1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 09:54:38 +00:00
riyadhafraa 42e57e6e0b Add a confirmation dialog before deleting a folder
Integrates an AlertDialog component for folder deletion, replacing the native confirm dialog, and updates corresponding locale files and test cases.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6a869b6f-2c2b-4d44-aab0-25864af19415
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 09:29:46 +00:00
riyadhafraa 3a8590cf8d Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 554658ce-6ac7-43ce-b72c-857eca56a28b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 09:25:10 +00:00
riyadhafraa 466a8c2340 Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
  - Refactored body into three vertical zones (per code-review feedback):
    1) static meta + recipient chips (`shrink-0`, always visible),
    2) replies-only scroll region (`flex-1 min-h-0 overflow-y-auto`,
       `data-testid="note-thread-scroll"`),
    3) static composer (`shrink-0`, pinned at the bottom).
  - Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
    `max-h-72` on `GroupedReplies` — the dedicated scroll zone handles
    overflow, avoiding nested double scrollbars.

Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
  - Seeds an owner note with 2 recipients and 30 alternating replies via
    SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
    box height ≤ 0.9 × viewport height, asserts the inner scroll region
    actually overflows (scrollHeight > clientHeight), and asserts the
    composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.

Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
2026-05-06 08:55:00 +00:00
riyadhafraa 889e24244c Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
  - Wrap the middle (sender info + recipients chips + replies block) in a
    new `flex-1 min-h-0 overflow-y-auto` scroller with
    `data-testid="note-thread-scroll"`.
  - Pin the reply composer with `shrink-0` so it stays visible while the
    middle scrolls.
  - Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
    `max-h-72` on `GroupedReplies` — the outer scroller handles overflow,
    avoiding nested double scrollbars.

Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
  - Seeds an owner note with 2 recipients and 30 alternating replies via
    SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
    box height ≤ 0.9 × viewport height, asserts the inner scroll region
    actually overflows (scrollHeight > clientHeight), and asserts the
    composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.

Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
2026-05-06 08:52:27 +00:00
riyadhafraa 01dfaccb53 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f51fcd22-676f-4c0f-986c-ce2839616b04
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 08:48:23 +00:00
riyadhafraa 60c23d4972 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:09:59 +00:00
riyadhafraa 10694f8d42 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:07:43 +00:00
riyadhafraa 6e49010940 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:05:12 +00:00
riyadhafraa b976c408a6 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:02:10 +00:00
riyadhafraa f562749214 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:57:57 +00:00
riyadhafraa 1889407565 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:55:32 +00:00
riyadhafraa 9fd4671327 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:52:54 +00:00
riyadhafraa 6826b475b2 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:50:39 +00:00
riyadhafraa 0bcfb9017d notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:46:33 +00:00
riyadhafraa 2b01cc1c24 notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:43:31 +00:00
riyadhafraa 4d720fdeed notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:41:22 +00:00
riyadhafraa fd194c5ed1 notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:39:03 +00:00
riyadhafraa ba0da3d26c notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL; pushed via @workspace/db.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query param computed by GROUP BY (drizzle correlated subquery
  was returning 0 — replaced with two queries merged in JS).
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic update for moves
  across all ["notes", ...] caches).
- UI: new FoldersRail (artifacts/tx-os/src/components/notes/folders-rail.tsx)
  rendered next to My Notes + Archive tabs; HTML5 draggable note cards with
  module-scoped DRAGGING_NOTE_ID fallback; visible drop highlight; rename +
  delete + create inline; folder-scoped + Unfiled filtering in notes.tsx.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: artifacts/tx-os/tests/notes-folders.spec.mjs covers create folder,
  drag note to folder, filter by folder/Unfiled, drag between folders,
  rename, delete a non-empty folder (verifies FK SET NULL + Unfiled count),
  cross-user folder rejection (400), and Arabic+RTL rendering.

Drift from plan: none — all originally scoped work shipped. Strengthened
e2e + cross-user check + tab-aware counts added per code review feedback.
2026-05-06 07:26:11 +00:00
riyadhafraa 6d3e38ec26 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: bda408e0-d5cf-4fb7-b04e-7a48c77fe40c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 07:08:39 +00:00
riyadhafraa 4258aa092c Task #410: floating draggable note popup + reply alert at sender
Convert the incoming-note popup from a centered AlertDialog modal into
a floating, draggable, animated card (no backdrop) and surface the
same card variant at the original sender when the recipient replies.

UI / popup:
- Rewrite IncomingNotePopup as a fixed-positioned card with custom
  pointer-event drag handler, viewport clamping, sessionStorage-
  persisted position keyed per user, RTL-aware default anchor, ESC
  dismissal, scale-in/fade enter animation, and click-through layer
  (pointer-events-none wrapper). Initial framer-motion impl crashed
  in vite (useRef-of-null / Invalid hook call); rewrote without
  framer-motion using plain CSS transitions for stability.
- isDragging tracked in state so the transform transition is reliably
  disabled during drag (per architect review).

Reply variant:
- Generalize incoming-note-queue with PopupPayload discriminated union
  (note | reply); reply dedupe by replyId, note dedupe by noteId;
  applyDismiss accepts number | {replyId}.
- Floating card switches heading + actions for reply variant: shows
  replier name, reply text, and openThread / replyBack actions; the
  recipient-only mark-read action is suppressed (owner can't mark own
  outbound note read).

Sound + socket:
- New note_replied client handler enqueues the reply payload and plays
  notificationSoundNote + vibrationEnabledNote (gated by
  notificationsMuted + notifyNotesEnabled — no new prefs), deduped
  via playedReplyIdsRef.
- Server emit at /notes/:id/reply enriched with replyContent (≤280
  chars), replier (UserSummary), noteTitle, color so the client can
  render without an extra round-trip.

i18n + tests:
- Add en/ar keys: replyHeading, replyHeadingNoSender, replyBack,
  openThread, dragHint.
- Extend notes-popup-on-receive.spec.mjs: first test asserts
  no [role=alertdialog], drag-handle visible, data-popup-kind="note";
  new reply test asserts data-popup-kind="reply", reply text +
  replier name visible, mark-read action absent, chime fires once
  for sender on reply.

Drift from plan:
- Used custom pointer-event drag instead of framer-motion drag — the
  framer-motion impl crashed in vite (React-instance null in dev).
  Same UX (drag from header only, click-through behind card).
- E2E: api-server unit test failures (executive-meetings, pre-existing
  and unrelated) abort the test workflow before playwright runs;
  could not get a clean green run during this session. Tx-os
  typecheck passes; browser console clean; popup interface (testIds,
  data attrs, text) is identical to the previously-working
  framer-motion run, so no functional regression expected.
2026-05-05 21:32:37 +00:00
riyadhafraa 71cfa14bf6 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 65c1a634-bf1c-4ec0-a1bb-0835f701dd68
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/G3ZvHke
Replit-Helium-Checkpoint-Created: true
2026-05-05 20:29:31 +00:00
riyadhafraa d8cb2c8dea feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention
model — sound + vibration + visual pulse + persistent queue indicator —
so recipients can't miss them.

Changes:
- DB: add notification_sound_note (default "knock"), notify_notes_enabled,
  vibration_enabled_note to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook (use-notifications-socket): play sound + vibrate on
  note_received, gated by mute/notifyNotesEnabled/socket warmup, with
  playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() returns boolean acceptance so the
  socket hook suppresses sound on own-note/duplicate.
- Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl,
  animate-in zoom, avatar animate-ping pulse.
- Persistent indicator on Notes app tile (home.tsx): badge =
  max(unread, queueLength), animate-ping ring while queueLength > 0,
  via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.
- Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount
  and __txosNotifLastSound for E2E observability.
- Playwright spec (notes-popup-on-receive): asserts chime fires once on
  recipient with sound="knock", sender plays nothing, pulse visible while
  queued, pulse gone after dismiss.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:31:44 +00:00
riyadhafraa d2b6e7f98c feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention
model — sound + vibration + visual pulse + persistent queue indicator —
so recipients can't miss them.

Changes:
- DB: add notification_sound_note (default "knock"), notify_notes_enabled,
  vibration_enabled_note to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook (use-notifications-socket): play sound + vibrate on
  note_received, gated by mute/notifyNotesEnabled/socket warmup, with
  playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() returns boolean acceptance so the
  socket hook suppresses sound on own-note/duplicate.
- Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl,
  animate-in zoom, avatar animate-ping pulse.
- Persistent indicator on Notes app tile (home.tsx): badge =
  max(unread, queueLength), animate-ping ring while queueLength > 0,
  via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.
- Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount
  and __txosNotifLastSound for E2E observability.
- Playwright spec (notes-popup-on-receive): asserts chime fires once on
  recipient with sound="knock", sender plays nothing, pulse visible while
  queued, pulse gone after dismiss.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:29:58 +00:00
riyadhafraa dcf90e1c2d feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention
model — sound + vibration + visual pulse + persistent queue indicator —
so recipients can't miss them.

Changes:
- DB: add notification_sound_note (default "knock"), notify_notes_enabled,
  vibration_enabled_note to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook (use-notifications-socket): play sound + vibrate on
  note_received, gated by mute/notifyNotesEnabled/socket warmup, with
  playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() returns boolean acceptance so the
  socket hook suppresses sound on own-note/duplicate.
- Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl,
  animate-in zoom, avatar animate-ping pulse.
- Persistent indicator on Notes app tile (home.tsx): badge =
  max(unread, queueLength), animate-ping ring while queueLength > 0,
  via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.
- Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount
  and __txosNotifLastSound for E2E observability.
- Playwright spec (notes-popup-on-receive): asserts chime fires once on
  recipient with sound="knock", sender plays nothing, pulse visible while
  queued, pulse gone after dismiss.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:28:20 +00:00
riyadhafraa 8ca324a557 feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention
model — sound + vibration + visual pulse + persistent queue indicator —
so recipients can't miss them.

Changes:
- DB: add notification_sound_note (default "knock"), notify_notes_enabled,
  vibration_enabled_note to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook (use-notifications-socket): play sound + vibrate on
  note_received, gated by mute/notifyNotesEnabled/socket warmup, with
  playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() returns boolean acceptance so the
  socket hook suppresses sound on own-note/duplicate.
- Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl,
  animate-in zoom, avatar animate-ping pulse.
- Persistent indicator on Notes app tile (home.tsx): badge =
  max(unread, queueLength), animate-ping ring while queueLength > 0,
  via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.
- Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount
  and __txosNotifLastSound for E2E observability.
- Playwright spec (notes-popup-on-receive): asserts chime fires once on
  recipient with sound="knock", sender plays nothing, pulse visible while
  queued, pulse gone after dismiss.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:26:13 +00:00
riyadhafraa aaadd9b520 feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention model
so recipients can't miss them.

Changes:
- DB: add `notification_sound_note` (default "knock"), `notify_notes_enabled`,
  `vibration_enabled_note` to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook: play notification sound + vibrate on `note_received`,
  gated by mute/notifyNotesEnabled/socket warmup, with playedNoteIdsRef
  dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() now returns boolean acceptance so
  the socket hook can suppress sound on own-note/duplicate.
- Popup visual: z-[110], ring-4 amber, shadow-2xl, animate-in zoom,
  avatar animate-ping pulse.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot tab
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:21:14 +00:00
riyadhafraa 33038106b4 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 07143651-1eb7-4f19-9d54-e529fe42f635
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/G3ZvHke
Replit-Helium-Checkpoint-Created: true
2026-05-05 16:12:46 +00:00
riyadhafraa 45609c81d6 Task #406: Pop the note onto the recipient's screen on arrival
- 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.
2026-05-05 15:45:17 +00:00
riyadhafraa 39d645ae4e Task #406: Pop the note onto the recipient's screen on arrival
- 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.
2026-05-05 15:43:48 +00:00
riyadhafraa 1e358ab32e Task #406: Pop the note onto the recipient's screen on arrival
- 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.
2026-05-05 15:39:39 +00:00
riyadhafraa 3d54270fdd Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d6173571-0cc3-4cd8-ac96-63f3c8723474
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/G3ZvHke
Replit-Helium-Checkpoint-Created: true
2026-05-05 15:31:05 +00:00
riyadhafraa eade4643e5 Add ability to view personal notes and update note schemas
Adds a new endpoint for listing user-specific notes and refines OpenAPI schemas for better data structure definition.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3565b59b-6890-4cf5-90a7-0c8041e15041
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/G3ZvHke
Replit-Helium-Checkpoint-Created: true
2026-05-05 15:28:41 +00:00
riyadhafraa 5b13fe16f5 Task #402: Convert Notes into in-app messaging
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.
2026-05-05 15:26:23 +00:00
riyadhafraa 73c9953675 Task #402: Convert Notes into in-app messaging
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.
2026-05-05 15:18:45 +00:00
riyadhafraa 672cb6280c Task #402: Convert Notes into in-app messaging
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.
2026-05-05 15:14:25 +00:00
riyadhafraa d1b4bb228c Task #402: Convert Notes into in-app messaging
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.
2026-05-05 15:09:44 +00:00
riyadhafraa 65c5a172d6 Task #402: Convert Notes into in-app messaging
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.
2026-05-05 15:01:18 +00:00
riyadhafraa 6352cbf844 Task #402: Convert Notes into in-app messaging (independence fix)
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.
2026-05-05 14:53:30 +00:00