Commit Graph

475 Commits

Author SHA1 Message Date
riyadhafraa 3d3a4ba972 notes: file-manager layout, folders section header, kill grid gap (#578)
Cleans up the Notes page after a user creates their first folder:

1. FoldersRail: in `rail` mode (every view except "All notes"), insert a
   small "My folders" / "مجلداتي" section header above the user's folder
   list so it no longer visually nests under the dark "Unfiled" pill.
   New i18n key `notes.folders.myFolders` in en.json and ar.json.

2. notes.tsx filteredNotes: when `folderSelection.kind === "all"`, hide
   notes whose `folderId !== null`. Combined with the existing
   FoldersBrowser cards this gives a file-manager feel — notes inside a
   folder are reachable by opening the folder card and no longer appear
   in the flat list, so users stop thinking move = copy. Counts on the
   "All notes" rail entry stay as the total (totalCount prop unchanged).

3. notes.tsx grid: replaced the two separate grid cells for
   composer/content with a single wrapper that uses `display: contents`
   on mobile (children flow into the outer grid honoring order-1/2/3)
   and `flex flex-col gap-4` on md+ (composer and content stack in the
   same cell, no empty grid-row gap when the FoldersRail is taller than
   the composer). Removed unused conditional row-start/row-span on the
   inner content div.

Out of scope per plan: dnd-kit behavior, shared-with-me logic, composer
or folder card redesign.

Validation: `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
Vite HMR successful after fix; no babel/JSX errors.
2026-05-18 08:00:28 +00:00
riyadhafraa d547d695e4 #577: clean up reschedule dialog layout on iPad landscape
The Postpone dialog's "إعادة الجدولة" tab was packing Date + Start +
End into a single 3-column row at `lg:` (≥1024 px), which on iPad
landscape (and small desktops) made iOS Safari's native date/time
spinner glyphs + Arabic AM/PM markers visibly collide between cells.
User reported the boxes looked "غير مرتبه".

Change:
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx:
  replace the `grid grid-cols-1 gap-3 lg:grid-cols-3` wrapper with a
  vertical `space-y-3` stack: Date input on its own full-width row,
  then a `grid grid-cols-2 gap-3` row holding Start + End side-by-
  side. Same `min-w-0` + `w-full` discipline on every cell so inputs
  respect their track.
- Updated the explanatory comment to record why we no longer try a
  3-column path: the only layout that stays clean from 320 px phones
  up through desktop without depending on a custom picker is the
  Date-on-top / Start+End-below stack.

Verified `tsc --noEmit` clean. No DB / locale / API changes. Mac
rebuild = `git pull && ./start.sh rebuild && docker compose up -d
--force-recreate web`. No script run needed.
2026-05-18 07:49:36 +00:00
riyadhafraa aba984d238 Improve handling of notification permission prompts
Update notification permission logic to differentiate between denied and dismissed states, and adjust locale messages accordingly.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 02bc349c-c9f1-47f4-967c-cfefeabde8bb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk
Replit-Helium-Checkpoint-Created: true
2026-05-17 17:21:18 +00:00
riyadhafraa 558d395e0c #576: diagnose push enable + open full edit page from quick actions
Three connected fixes on the Executive Meetings schedule.

1) Push enable diagnostics (use-push-subscription.ts):
   - `enable()` now returns { ok, reason } instead of a bare boolean.
   - Reasons: unsupported, unsupported_ios_safari, permission_denied,
     vapid_unavailable, subscribe_failed, server_rejected.
   - Detects iPhone/iPad Safari not running as a PWA (checks
     navigator.standalone + display-mode media query, with iPadOS
     MacIntel + maxTouchPoints fallback) and short-circuits with
     unsupported_ios_safari so the user gets a concrete
     "open from Home Screen first" message instead of a silent fail.
   - Wrapped VAPID fetch and pushManager.subscribe in their own
     try/catches so each failure mode maps to its own reason.

2) Specific failure toasts (push-enable-prompt.tsx,
   notification-settings.tsx):
   - Both call sites now look up
     `notifSettings.push.errors.<reason>` and fall back to the
     legacy generic copy if the key is missing.
   - Added enableSuccess toast on the toggle path too — previously
     only the prompt-card path showed feedback on success.
   - New AR + EN strings under notifSettings.push.errors.*.

3) Quick-actions row sheet (executive-meetings.tsx):
   - Edit + Postpone buttons now sit SIDE BY SIDE on every viewport
     (flex + flex-1 + min-w-0 instead of flex-wrap + min-w-[11rem]),
     same height, equal width — fixes the stacked / over-sized Edit
     button shown in the user's iPhone screenshot.
   - "Edit" no longer opens the small Meeting edit Dialog. It now
     flips the whole schedule into edit mode (same as the toolbar
     pencil) and paints a transient accent ring on the originating
     row (~1.6s) + smooth-scrolls it into view. Matches image 3 in
     #576 where the user expects to see the full editable list.
   - Added `highlightEdit` prop on MeetingRow that composes an
     extra inset+outer ring on top of the existing quickOpen /
     selection / current / press shadows, plus a soft background
     tint, and clears automatically when ScheduleSection clears
     highlightEditRowId.

Out of scope (left for follow-up tasks): server-side push delivery
(payloads/sounds), the edit-mode UI itself.
2026-05-17 17:19:42 +00:00
riyadhafraa 039e251c6a #575: IT-P brand icon + login hero image
Replace generic PWA/favicon assets with the IT-P brand image and layer
the same image over the login page's AnimatedArt panel.

- Generated icon-192/512, maskable-512, and apple-touch-icon (180) from
  the attached IT-P photo via ImageMagick (center crop 1110x1110, then
  resize). Maskable has #0B1E3F padding so the safe zone is respected
  on Android adaptive icons.
- Copied source as public/brand-itp.jpg for the login hero.
- index.html: replaced the generic favicon.svg link with a full
  favicon set built from IT-P (favicon.ico + 16/32/48/192/512 PNG),
  all cache-busted with ?v=itp1. Deleted public/favicon.svg so no
  browser can fall back to the old generic mark.
- Cache-busted apple-touch-icon link the same way so iPad PWA
  re-installs pick up the new artwork.
- manifest.webmanifest: same ?v=itp1 cache-bust on all icon entries.
- login.tsx: layered IT-P image over AnimatedArt with mix-blend-screen
  (opacity 90%) plus top/bottom and right-edge dark gradients so the
  composition reads as one piece and the right edge fades cleanly into
  the form panel. Still hidden on < lg per scope.

Did not change favicon.svg itself (kept as last-resort vector
fallback). Mobile (< lg) shows no hero — explicit choice per plan
("إخفاؤها بشكل مدروس").
2026-05-17 16:09:24 +00:00
riyadhafraa 192f412f2a #575: IT-P brand icon + login hero image
Replace generic PWA/favicon assets with the IT-P brand image and layer
the same image over the login page's AnimatedArt panel.

- Generated icon-192/512, maskable-512, and apple-touch-icon (180) from
  the attached IT-P photo via ImageMagick (center crop 1110x1110, then
  resize). Maskable has #0B1E3F padding so the safe zone is respected
  on Android adaptive icons.
- Copied source as public/brand-itp.jpg for the login hero.
- index.html: added explicit PNG icon links (192, 512) before the
  existing favicon.svg fallback, plus cache-bust query (?v=itp1) on
  apple-touch-icon so iPad PWA re-installs pick up the new artwork.
- manifest.webmanifest: same ?v=itp1 cache-bust on all icon entries.
- login.tsx: layered IT-P image over AnimatedArt with mix-blend-screen
  (opacity 90%) plus top/bottom and right-edge dark gradients so the
  composition reads as one piece and the right edge fades cleanly into
  the form panel. Still hidden on < lg per scope.

Did not change favicon.svg itself (kept as last-resort vector
fallback). Mobile (< lg) shows no hero — explicit choice per plan
("إخفاؤها بشكل مدروس").
2026-05-17 16:08:28 +00:00
riyadhafraa 2f591a6ba8 Show the notifications app on the main page again
Remove hardcoded exclusion for the notifications app in the home page component.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 58af0916-ab79-457c-868e-3b72336f6067
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk
Replit-Helium-Checkpoint-Created: true
2026-05-17 15:42:29 +00:00
riyadhafraa b5efd9eb12 Add system updates page to admin section
Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1e6de894-7c78-4557-b01a-a2c1c01f4155
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk
Replit-Helium-Checkpoint-Created: true
2026-05-17 14:59:36 +00:00
riyadhafraa 191e072353 #571: ship Calendar/Notifications disabled by default + drop Notifications tile from home
Task: on fresh installs the Calendar and Notifications apps were
appearing enabled by default, and the home screen showed a
Notifications tile that was redundant with the bell icon already in
the top bar. User wanted both apps disabled by default and the home
tile gone (bell stays).

Changes:
- scripts/src/seed.ts: flip the seed `isActive` for `notifications`
  and `calendar` from true → false. The seed inserts apps with
  `db.insert(appsTable).values(apps).onConflictDoNothing()`, so this
  is fully idempotent: existing environments (where the row already
  exists with isActive=true) are NOT changed; only fresh inserts on
  new installs pick up the disabled default. Admins can enable
  either app from app settings when they want it.
- artifacts/tx-os/src/pages/home.tsx: remove "notifications" from
  the `dockApps` filter so the bottom dock no longer renders a
  Notifications tile. Filter becomes `["services", "admin"]`. The
  top-bar bell icon (and its unread badge) was untouched and still
  routes to `/notifications`.

Out of scope (per spec): no migration to disable existing prod
rows; no role/permission changes; bell icon in the top bar stays.

Files:
- scripts/src/seed.ts (notifications block ~L229, calendar block ~L275)
- artifacts/tx-os/src/pages/home.tsx:532
2026-05-17 14:43:54 +00:00
riyadhafraa 2aa4a12ddc #571: ship Calendar/Notifications disabled by default + drop Notifications tile from home
Task: on fresh installs the Calendar and Notifications apps were
appearing enabled by default, and the home screen showed a
Notifications tile that was redundant with the bell icon already in
the top bar. User wanted both apps disabled by default and the home
tile gone (bell stays).

Changes:
- scripts/src/seed.ts: flip the seed `isActive` for `notifications`
  and `calendar` from true → false. The seed inserts apps with
  `db.insert(appsTable).values(apps).onConflictDoNothing()`, so this
  is fully idempotent: existing environments (where the row already
  exists with isActive=true) are NOT changed; only fresh inserts on
  new installs pick up the disabled default. Admins can enable
  either app from app settings when they want it.
- artifacts/tx-os/src/pages/home.tsx: remove "notifications" from
  the `dockApps` filter so the bottom dock no longer renders a
  Notifications tile. Filter becomes `["services", "admin"]`. The
  top-bar bell icon (and its unread badge) was untouched and still
  routes to `/notifications`.

Out of scope (per spec): no migration to disable existing prod
rows; no role/permission changes; bell icon in the top bar stays.

Files:
- scripts/src/seed.ts (notifications block ~L229, calendar block ~L275)
- artifacts/tx-os/src/pages/home.tsx:532
2026-05-17 14:42:48 +00:00
riyadhafraa c40384b0d9 #570: stronger selected-row highlight + Edit button in Quick Actions
Task: on iPad the bulk-selected row was nearly invisible (only a 2px
inset navy ring), and the row-tap Quick Actions dialog had only a
Postpone button — user asked for a real visible selection fill and
an Edit (pencil) action wired to the existing edit-meeting flow.

Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- MeetingRow: add `selectionBg = rgba(11,30,63,0.18)` painted on the
  row's `<tr>` backgroundColor in addition to the existing
  `selectionShadow` 2px navy ring. Composition order in trStyle is
  `quickOpenBg ?? selectionBg ?? baseBg` so an open quick-actions
  surface still wins (transient action state), user-picked row
  colours still show when nothing is selected, and the navy tint
  reads clearly on iPad against the table grid.
- MeetingRow: new optional `onQuickEdit?: () => void` prop. Quick
  Actions Dialog gains a pencil-icon "Edit" button before the
  existing Postpone button, wrapped in `flex flex-wrap gap-2` so the
  two buttons sit side-by-side on iPad/desktop and stack on narrow
  viewports. Button only renders when a handler is wired
  (defensive — `quickActionsCanMutate` already gates the surface).
- ScheduleSection: host a local `editingMeeting` + `editingSaving`
  state plus a `MeetingFormDialog` mount, with a save handler whose
  body mirrors ManageSection's `save()` exactly (same PATCH payload
  projection, same `wrapAsParagraph` / attendee shaping, same
  toast/error translation). Edit button on a row opens this in-place
  dialog instead of switching the user to the Manage tab.
- Locale: add `executiveMeetings.quickActions.edit` = "تعديل" / "Edit".

Notes:
- No new icon import needed — `Pencil` was already in the lucide
  import group.
- Selection ring (`#0B1E3F`) and tint use the same navy so they
  read as one cohesive selected state, not competing layers.
- Pre-existing `use-push-subscription.ts` TS noise unchanged.
2026-05-17 14:40:34 +00:00
riyadhafraa dc30d75e11 Hide redundant clock and navigation elements for president view
Modify executive-meetings.tsx to conditionally hide the HeaderClock and section navigation when isPresidentView is true.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e1c6509c-638c-4c43-8d4f-4b4a5910fc4a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fNoqq4C
Replit-Helium-Checkpoint-Created: true
2026-05-17 14:34:59 +00:00
riyadhafraa 21dda3d372 #572 president-only focused view for executive meetings
User asked that when the President (`executive_ceo` role) logs into
the Meetings page he sees a clean "presentation" layout: just the
schedule table, an analog clock plus the weekday/date of the
schedule, and nothing else. The Manage / Notifications / Audit /
PDF tabs and the PDF-export shortcut button are noisy for him and
must disappear — but his backend permissions stay intact.

Changes (frontend only — backend role gates untouched):
- New component `components/executive-meetings/analog-clock.tsx`:
  small SVG analog clock (hour/minute/second hands, navy + gold),
  ticks every second, ~44px default.
- New inner `PresidentClockBar` component in `executive-meetings.tsx`:
  renders the analog clock + weekday + **digital time** (ticking
  per second, localized) + selected schedule date in one block.
- `pages/executive-meetings.tsx`:
  - Derived `isPresidentView` from the `/me` roles: true iff
    `executive_ceo` is present AND the user is NOT also `admin`
    or `executive_office_manager` (admins keep full UI).
  - Effect forces `section = "schedule"` whenever
    `isPresidentView` becomes true, so the president can never
    land on a now-hidden tab.
  - Hides the header PDF-shortcut button and the entire SECTIONS
    tab list when `isPresidentView`. The `#em-toolbar-portal` is
    kept so the date picker still mounts.
  - `ScheduleSection` accepts two new optional props:
    `isPresidentView` and `lang`. When `isPresidentView` is on,
    the toolbar renders the AnalogClock next to a label showing
    `formatWeekday(date, lang, "long")` and `formatDate(date, lang)`
    (e.g. "Sunday · May 17, 2026" / "الأحد · 17 مايو 2026"). The
    label updates immediately when the date input changes.
  - Edit-mode toggle stays gated on `canMutate`, so the president
    (who has canMutate) still gets the inline-edit pencil if he
    wants it.

Out of scope: backend role changes, applying the focused view to
other roles, manual presentation-mode toggle. Verified via tsc
(only the pre-existing `use-push-subscription` error remains).
2026-05-17 14:34:12 +00:00
riyadhafraa 27384d019a #572 president-only focused view for executive meetings
User asked that when the President (`executive_ceo` role) logs into
the Meetings page he sees a clean "presentation" layout: just the
schedule table, an analog clock plus the weekday/date of the
schedule, and nothing else. The Manage / Notifications / Audit /
PDF tabs and the PDF-export shortcut button are noisy for him and
must disappear — but his backend permissions stay intact.

Changes (frontend only — backend role gates untouched):
- New component `components/executive-meetings/analog-clock.tsx`:
  small SVG analog clock (hour/minute/second hands, navy + gold),
  ticks every second, ~44px default.
- `pages/executive-meetings.tsx`:
  - Derived `isPresidentView` from the `/me` roles: true iff
    `executive_ceo` is present AND the user is NOT also `admin`
    or `executive_office_manager` (admins keep full UI).
  - Effect forces `section = "schedule"` whenever
    `isPresidentView` becomes true, so the president can never
    land on a now-hidden tab.
  - Hides the header PDF-shortcut button and the entire SECTIONS
    tab list when `isPresidentView`. The `#em-toolbar-portal` is
    kept so the date picker still mounts.
  - `ScheduleSection` accepts two new optional props:
    `isPresidentView` and `lang`. When `isPresidentView` is on,
    the toolbar renders the AnalogClock next to a label showing
    `formatWeekday(date, lang, "long")` and `formatDate(date, lang)`
    (e.g. "Sunday · May 17, 2026" / "الأحد · 17 مايو 2026"). The
    label updates immediately when the date input changes.
  - Edit-mode toggle stays gated on `canMutate`, so the president
    (who has canMutate) still gets the inline-edit pencil if he
    wants it.

Out of scope: backend role changes, applying the focused view to
other roles, manual presentation-mode toggle. Verified via tsc
(only the pre-existing `use-push-subscription` error remains).
2026-05-17 14:32:19 +00:00
riyadhafraa 4b151c50a8 #568 silent delete for finished orders
User requested that deleting a finished order from My Orders happen
silently — one click on the trash icon, no confirmation dialog, no
success toast. Just click and the card disappears.

Changes:
- Removed the delete-confirm AlertDialog from OrderCard along with
  its `confirmDeleteOpen` state. The trash icon now calls
  `handleDelete` directly.
- Removed `toast({ title: t("myOrders.deleted") })` from
  `scheduleDelete`. The 7-second soft-delete grace window stays in
  place (pendingDeleteIds filter + setTimeout) so the API DELETE
  still fires after the window. The `deleteFailed` error toast is
  preserved so silent API failures don't lose orders.
- Dropped the now-unused locale keys from ar.json and en.json:
  `deleteConfirmTitle`, `deleteConfirmBody`, `deleteConfirm`,
  `deleted`.
- Removed the unused `AlertDialogDescription` import (the remaining
  cancel-confirm dialog uses only the title).

Cancel-order flow remains untouched (still shows confirm dialog and
undo toast as in #566).
2026-05-17 14:13:42 +00:00
riyadhafraa 4c57c58f8c #566 clean up My Orders page (3 removals)
Per user request on the طلباتي page:

1. Removed the "Clear finished orders" bulk button entirely. The button
   was showing as a raw i18n key (myOrder c...) for the user and they
   asked for it gone. Dropped the related AlertDialog, confirmClearOpen
   state, handleClearFinished handler, and the locale keys
   clearFinished_*, clearConfirmTitle/Body_*, clearConfirm,
   clearedCount_*, and clearedPartial in both ar.json and en.json.

2. Removed the "تراجع / Undo" ToastAction shown after deleting an
   order. The toast now displays only the "Order deleted" title; the
   7-second timer + pendingDeleteIds soft-delete window is preserved
   so the actual DELETE still fires after the grace period. The undo
   closure was deleted since nothing references it anymore. Single-id
   call site stays untouched (scheduleDelete still receives [id]).

3. Removed the "ستتاح لك ثوانٍ قليلة للتراجع" description from the
   cancel-order confirmation dialog. The cancelConfirmBody key was
   dropped from both locales. The AlertDialog now shows only the
   title + the two action buttons.

Out of scope (kept as-is):
- The undo ToastAction after CANCEL (user only asked about delete).
- Incoming-orders page.

Notes:
- ToastAction import is still used by the cancel-undo path.
- Trash2 import still used by the per-card delete button.
- Pre-existing TS error in use-push-subscription.ts is unrelated.
2026-05-17 14:07:23 +00:00
riyadhafraa 4e5bed602e fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on
iPad/mobile. Six fixes, all client-side:

1. Quick-actions surface now opens from ANY click on a meeting row
   (was: only the attendees cell). Root cause: handleRowClick guard
   skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*`
   descendants. Those affordances only do anything in edit mode, but
   handleRowClick already early-returns when edit mode is on, so the
   testid guard was always over-aggressive. Dropped those guards plus
   the `[role='button']` rule from the interactive selector, kept the
   real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc.
   guards and the `em-row-actions-*` / `em-row-select-*` testid
   guards (those affordances are visible outside edit mode and must
   not trigger the surface).
   File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350).

2. Originating row is now unmistakable on iPad: quickOpenShadow is
   `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single
   3px inset) and the row background tints to
   `rgba(highlight, 0.08)` while quick-actions is open.
   File: artifacts/tx-os/src/pages/executive-meetings.tsx
   (~L4287, L4344).

3. Postpone dialog header was overlapping the Radix close X in RTL.
   Added `pr-8` on the DialogHeader (same pattern already used for
   the row-quick mini-dialog).
   File: artifacts/tx-os/src/components/executive-meetings/
   upcoming-meeting-alert.tsx (~L1447).

4. Reschedule tab: date / start / end inputs collided on iPad
   portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column
   through iPad portrait, three across on landscape/desktop), with
   `min-w-0` on each cell and `w-full` on every Input so Safari's
   native date/time pickers stop overflowing.
   Same file (~L1761).

5. Cancel tab X overlap is resolved by the same DialogHeader fix in
   step 3 (one Dialog wraps all three tabs).

6. Removed the `cancelHint` paragraph + the `cancelHint` translation
   keys in ar.json (L1252) and en.json (L1164) — fully unused now.

NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky).
Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this).
2026-05-17 10:19:16 +00:00
riyadhafraa 620ab284e2 fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on
iPad/mobile. Six fixes, all client-side:

1. Quick-actions surface now opens from ANY click on a meeting row
   (was: only the attendees cell). Root cause: handleRowClick guard
   skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*`
   descendants. Those affordances only do anything in edit mode, but
   handleRowClick already early-returns when edit mode is on, so the
   testid guard was always over-aggressive. Dropped those guards plus
   the `[role='button']` rule from the interactive selector, kept the
   real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc.
   guards and the `em-row-actions-*` / `em-row-select-*` testid
   guards (those affordances are visible outside edit mode and must
   not trigger the surface).
   File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350).

2. Originating row is now unmistakable on iPad: quickOpenShadow is
   `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single
   3px inset) and the row background tints to
   `rgba(highlight, 0.08)` while quick-actions is open.
   File: artifacts/tx-os/src/pages/executive-meetings.tsx
   (~L4287, L4344).

3. Postpone dialog header was overlapping the Radix close X in RTL.
   Added `pr-8` on the DialogHeader (same pattern already used for
   the row-quick mini-dialog).
   File: artifacts/tx-os/src/components/executive-meetings/
   upcoming-meeting-alert.tsx (~L1447).

4. Reschedule tab: date / start / end inputs collided on iPad
   portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column
   through iPad portrait, three across on landscape/desktop), with
   `min-w-0` on each cell and `w-full` on every Input so Safari's
   native date/time pickers stop overflowing.
   Same file (~L1761).

5. Cancel tab X overlap is resolved by the same DialogHeader fix in
   step 3 (one Dialog wraps all three tabs).

6. Removed the `cancelHint` paragraph + the `cancelHint` translation
   keys in ar.json (L1252) and en.json (L1164) — fully unused now.

NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky).
Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this).
2026-05-17 10:18:13 +00:00
riyadhafraa 51c3d8a8ba Fix toggle switch behavior in right-to-left reading modes
Update the switch component's CSS to correctly position the toggle thumb in RTL layouts for both checked and unchecked states.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ee6229a6-5472-4ca0-8f84-74b229703438
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX
Replit-Helium-Checkpoint-Created: true
2026-05-16 17:46:43 +00:00
riyadhafraa f4253d4d9d Improve toggle switch behavior in right-to-left languages
Add custom RTL and LTR variants to Tailwind CSS configuration to correctly position toggle switch indicators in Arabic and other right-to-left languages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 84451adc-9c86-40d5-80ba-846e7ea2a813
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX
Replit-Helium-Checkpoint-Created: true
2026-05-16 17:21:39 +00:00
riyadhafraa bf39a6eda6 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe`, and a spec-aligned alias
  `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`, gated on
  `serviceWorker` AND `PushManager` support so older browsers no-op
  cleanly.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- Architect rounds addressed in order:
  1. race + payload size — fixed.
  2. dedup gate + first-launch UX + SW base-path + tests — fixed.
  3. DELETE alias + PushManager check — fixed in this commit. A true
     410-cleanup test was attempted but reaching the prune branch from
     an out-of-process .mjs test requires mocking the `web-push`
     module, which the current `node --test` + bundled-dist harness
     doesn't support cleanly; the 404/410 catch branch is small,
     self-contained, and was code-reviewed. Tracked as a follow-up.
2026-05-16 15:35:42 +00:00
riyadhafraa 8f3b750961 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- Two architect rounds: first flagged race + payload size (fixed),
  second flagged dedup gate + first-launch UX + SW base-path + tests
  (all fixed in this commit).
2026-05-16 15:29:28 +00:00
riyadhafraa 243ccb3e00 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker
    volume) with /tmp fallback for Replit dev, else ephemeral in-memory.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes so over-sized notes don't kill delivery.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (account switch on shared
    device) so the previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into the 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only
  (no asset caching). Focuses an existing tab or opens a new one at the
  payload URL.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status:
  unsupported / denied / default / subscribed).
- New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings
  (notifSettings.push.*). iPad copy explains that the user must add to
  Home Screen first for iOS to allow Web Push.

Plumbing
- OpenAPI: 3 new operations under `notifications` tag; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the generated
  key; subscribe endpoint 401s without auth as expected.
- Architect review surfaced two HIGH issues (account-switch leak,
  payload size); both fixed before completion.
2026-05-16 15:26:18 +00:00
riyadhafraa e9d7b8dc76 Fix service images + Arabic ي dots on home tiles (Task #552)
Two visible bugs reported from the iPad after Task #551 deploy:

1) Only "قهوة سعودي" showed its image on /services. Three other
   seeded services (شاي، بلاك كوفي، عصير طازج) showed a broken-image
   placeholder because the seed only set `imageUrl` for Saudi Coffee.

2) The dots under final ي in the home-grid tile label "خدماتي"
   didn't render on iOS Safari. The label `<span>` didn't pin a
   font-family, so iOS fell back to a Latin face that lacks proper
   Arabic glyphs.

Changes:

- scripts/src/seed.ts:
  • Added `imageUrl` for tea/black-coffee/juice in the insert array
    (covers fresh installs).
  • Added a backfill loop AFTER the insert that fills `imageUrl` on
    existing rows only when the value is currently NULL. Admin-
    customized image URLs are never overwritten — matches the
    existing legacy-rename pattern at lines 395-423.

- artifacts/tx-os/src/pages/home.tsx (AppIconContent label):
  • Added `lang` and `dir` attributes on the tile `<span>`.
  • Pinned an explicit font stack: DIN Next LT Arabic → Helvetica
    Neue LT Arabic → Tajawal for Arabic; Helvetica Neue → system-ui
    for English. The three Arabic faces are already declared in
    custom-fonts.css.

No schema changes, no new dependencies. Verified typecheck passes
and HMR reloaded both files cleanly in the dev server.

Push to Gitea is a separate follow-up (git commit is restricted in
the main agent; platform commits this task's changes on completion,
and the push task can then mirror them to the Mac).
2026-05-16 10:18:07 +00:00
riyadhafraa a6d5988421 Add PWA manifest + icons so iPad opens Tx OS full-screen (Task #550)
Problem: on the iPad the Safari URL bar stayed visible even after
"Add to Home Screen" because index.html had no PWA manifest and no
Apple PWA meta tags — the home-screen icon opened a normal Safari tab.

Changes:
- New `artifacts/tx-os/public/manifest.webmanifest`:
  name/short_name "Tx OS", start_url "/", scope "/",
  display "standalone", background_color and theme_color "#0B1E3F"
  (matches the Meetings brand), and 3 icons (192/512 "any" + 512
  "maskable" with safe-area padding).
- Generated icon PNGs from the existing `public/favicon.svg` brand
  mark via ImageMagick:
    public/icons/icon-192.png            (192x192)
    public/icons/icon-512.png            (512x512)
    public/icons/icon-maskable-512.png   (512x512, 360x360 logo
                                          centered on #0B1E3F so iOS/
                                          Android masks don't crop)
    public/apple-touch-icon.png          (180x180, iPad/iPhone home)
- `artifacts/tx-os/index.html` head: added <link rel="manifest">,
  <link rel="apple-touch-icon">, plus apple-mobile-web-app-capable,
  mobile-web-app-capable, apple-mobile-web-app-status-bar-style
  (default), apple-mobile-web-app-title, and theme-color meta tags.
- `replit.md`: new "iPad / iPhone — open Tx OS full-screen" section
  with the one-time Add-to-Home-Screen steps and the gotcha that
  pre-existing shortcuts must be removed and re-added to pick up the
  manifest.

Verified: dev server returns 200 for /manifest.webmanifest,
/apple-touch-icon.png, and /icons/icon-192.png. The new files all
live under public/ so they're copied verbatim into the nginx image
on the next docker build.
2026-05-16 08:51:25 +00:00
riyadhafraa 40b5f7773c Update website's default sharing image
Update opengraph.jpg in the public artifacts directory.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6a7af115-92ba-48dc-a8b6-068ee87c1c7d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/FvHcc7z
Replit-Helium-Checkpoint-Created: true
2026-05-14 07:00:21 +00:00
riyadhafraa 7a2ae8434d Update project documentation and code comments to remove platform-specific references
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu
Replit-Helium-Checkpoint-Created: true
2026-05-14 06:23:49 +00:00
riyadhafraa 3af737186d Improve admin modal design and accessibility with shared component
Refactors AdminFormDialog to create a reusable component for admin modals, including aria-labelledby/describedby attributes and Escape key closing functionality.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fa532550-4873-409b-840a-ac4c474132f3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu
Replit-Helium-Checkpoint-Created: true
2026-05-13 19:11:10 +00:00
riyadhafraa a15365f459 Task #531: Redesign DeletionWarningDialog for a more professional look
Scope: artifacts/tx-os/src/pages/admin.tsx — the shared confirm
dialog used for deleting Apps, Services, and Users on the admin
page.

What changed
- Three explicit visual regions instead of a flat stack:
  - Header: AlertTriangle icon in a destructive-tinted circle next
    to the bold title (which already contains the item name from
    the existing translation strings).
  - Body: warning copy, then impacted items rendered inside a
    sub-card (rounded border + subtle bg + custom bullet dots
    instead of `list-disc`), then a destructive-tinted callout
    holding the "cannot be undone" hint.
  - Footer: separated by a top border and a faint bg tint;
    Cancel = ghost (quieter), Confirm/Anyway = destructive (clear
    primary action). When `isPending`, a small spinner appears
    inside the destructive button.
- Slightly larger max-width (max-w-md), shadow-xl, and overflow-
  hidden so the rounded corners read cleanly with the new footer
  divider.
- `role="alertdialog"` + `aria-modal="true"` added on the panel.

Behaviour preserved
- Component props, call sites, and all `data-testid` hooks are
  unchanged (testId, confirmTestId — covers app-delete-dialog/
  -confirm, service-delete-dialog/-confirm, user-delete-dialog/
  -confirm).
- No translation keys added; reuses every existing
  admin.deleteApp/Service/User.* string.
- Cancel renders before Confirm in the DOM so tab order is
  Cancel -> Destructive (no a11y regression). Visual order is
  controlled with `justify-end`, which mirrors correctly in RTL.
- Pure UI change: no API/handler/permission logic touched.

Notes
- Architect flagged an earlier `flex-row-reverse` footer for
  putting the destructive button first in tab order; fixed before
  marking complete.
- Pre-existing TS18046 / TS2345 errors in admin.tsx are unrelated.
2026-05-13 15:50:54 +00:00
riyadhafraa 21ccb6c426 Task #531: Redesign DeletionWarningDialog for a more professional look
Scope: artifacts/tx-os/src/pages/admin.tsx — the shared confirm
dialog used for deleting Apps, Services, and Users on the admin
page.

What changed
- Three explicit visual regions instead of a flat stack:
  - Header: AlertTriangle icon in a destructive-tinted circle next
    to the bold title (which already contains the item name from
    the existing translation strings).
  - Body: warning copy, then impacted items rendered inside a
    sub-card (rounded border + subtle bg + custom bullet dots
    instead of `list-disc`), then a destructive-tinted callout
    holding the "cannot be undone" hint.
  - Footer: separated by a top border and a faint bg tint;
    Cancel = ghost (quieter), Confirm/Anyway = destructive (clear
    primary action). When `isPending`, a small spinner appears
    inside the destructive button.
- Slightly larger max-width (max-w-md), shadow-xl, and overflow-
  hidden so the rounded corners read cleanly with the new footer
  divider.
- `role="alertdialog"` + `aria-modal="true"` added on the panel.

Behaviour preserved
- Component props, call sites, and all `data-testid` hooks are
  unchanged (testId, confirmTestId — covers app-delete-dialog/
  -confirm, service-delete-dialog/-confirm, user-delete-dialog/
  -confirm).
- No translation keys added; reuses every existing
  admin.deleteApp/Service/User.* string.
- Cancel renders before Confirm in the DOM so tab order is
  Cancel -> Destructive (no a11y regression). Visual order is
  controlled with `justify-end`, which mirrors correctly in RTL.
- Pure UI change: no API/handler/permission logic touched.

Notes
- Architect flagged an earlier `flex-row-reverse` footer for
  putting the destructive button first in tab order; fixed before
  marking complete.
- Pre-existing TS18046 / TS2345 errors in admin.tsx are unrelated.
2026-05-13 15:49:47 +00:00
riyadhafraa 60fbd3dd84 Task #529: Restore topbar logout + collapsible Settings groups
home.tsx
- Re-import LogOut from lucide-react.
- Append a single-tap Logout icon button after <SettingsPanel /> in
  the right-side cluster, wired to the existing handleLogout flow.
- Drop the onLogout prop from <SettingsPanel />; update the topbar
  comment block to describe the Bell -> Inbox -> Settings -> Logout
  cluster.

settings-panel.tsx
- Drop the LogOut import, the onLogout prop on SettingsPanel and
  SettingsPanelBody, the local handleLogout wrapper, and the inline
  logout button (data-testid="settings-logout").
- Wrap the four groups (language, notifications, sound, clock) in
  the existing shadcn Accordion (type="multiple") so each toggles
  independently. Default = all collapsed; open state is lifted to
  SettingsPanel via useState<GroupId[]> so toggles persist while the
  panel is closed and re-opened in the same session.
- New GroupItem helper styles each AccordionItem like the prior
  GroupCard so the panel keeps its rounded-card visual rhythm. The
  trigger uses text-start so headers align correctly in RTL.
- Existing form bodies (LanguageBody, NotificationSettingsContent,
  SoundBody, ClockStyleContent) are reused unchanged, preserving all
  DB + localStorage persistence.

Notes
- Logout placement and removal from the panel were both explicit user
  requests (clarified in plan #529 after #527 unified the topbar).
- No new i18n keys; existing settingsPanel.section.* labels reused
  as accordion triggers.
- typecheck clean (pre-existing TS6305/TS7006 noise unrelated).
2026-05-13 15:15:31 +00:00
riyadhafraa 7abb0c870b Task #527: unify topbar into per-user Settings panel
Topbar right cluster collapses from 5–7 icons to exactly Bell -> Inbox
(conditional on canReceiveOrders) -> Settings (gear). Logout moved
inside the Settings panel; the second bell, globe, volume and clock
buttons are gone from the topbar.

New SettingsPanel renders as a Popover on md+ and a bottom Sheet on
<md (via new useMediaQuery hook). It composes existing controls
rather than re-implementing them:
- Language radio uses useUpdateLanguage + i18n.changeLanguage
- Notifications group renders extracted NotificationSettingsContent
- Sound group toggles notificationsMuted (same flag QuickMute used)
- Clock group renders extracted ClockStyleContent
All DB + localStorage persistence is preserved unchanged.

Refactors:
- notification-settings.tsx: extracted NotificationSettingsContent;
  NotificationSettingsPicker now wraps it (kept for any future caller).
- clock-style-picker.tsx: extracted ClockStyleContent; ClockStylePicker
  now wraps it; removed dead setOpen reference inside choose().
- home.tsx: removed Globe / QuickMute / NotificationSettingsPicker /
  ClockStylePicker / standalone LogOut from the cluster, dropped
  related imports + useUpdateLanguage, reordered to Bell -> Inbox ->
  Settings, and passes onLogout to SettingsPanel.

i18n: added settingsPanel.{title, section.*, lang.*, sound.master}
keys to ar.json and en.json.

Code review: PASS.
2026-05-13 14:55:53 +00:00
riyadhafraa e6a3b148f4 Task #527: unify topbar into per-user Settings panel
Topbar right cluster collapses from 5–7 icons to exactly Bell -> Inbox
(conditional on canReceiveOrders) -> Settings (gear). Logout moved
inside the Settings panel; the second bell, globe, volume and clock
buttons are gone from the topbar.

New SettingsPanel renders as a Popover on md+ and a bottom Sheet on
<md (via new useMediaQuery hook). It composes existing controls
rather than re-implementing them:
- Language radio uses useUpdateLanguage + i18n.changeLanguage
- Notifications group renders extracted NotificationSettingsContent
- Sound group toggles notificationsMuted (same flag QuickMute used)
- Clock group renders extracted ClockStyleContent
All DB + localStorage persistence is preserved unchanged.

Refactors:
- notification-settings.tsx: extracted NotificationSettingsContent;
  NotificationSettingsPicker now wraps it (kept for any future caller).
- clock-style-picker.tsx: extracted ClockStyleContent; ClockStylePicker
  now wraps it; removed dead setOpen reference inside choose().
- home.tsx: removed Globe / QuickMute / NotificationSettingsPicker /
  ClockStylePicker / standalone LogOut from the cluster, dropped
  related imports + useUpdateLanguage, reordered to Bell -> Inbox ->
  Settings, and passes onLogout to SettingsPanel.

i18n: added settingsPanel.{title, section.*, lang.*, sound.master}
keys to ar.json and en.json.

Code review: PASS.
2026-05-13 14:53:53 +00:00
riyadhafraa 04f2ab7ea9 Task #527: Unify topbar prefs into per-user Settings panel
- New `settings-panel.tsx` consolidates four duplicate topbar buttons
  (clock-style picker, quick-mute, notification-settings popover,
  language-toggle globe) behind a single gear icon that opens a
  right-side Sheet (full-width on mobile, capped on desktop).
- Panel groups: Language (ar/en), Notifications (per-slot
  enabled/sound/vibration), Sound (master mute), Clock (visibility,
  12/24h, style).
- Reuses existing hooks (useUpdateLanguage, useUpdate{Clock*},
  useUpdatePrefs, useHome/TopbarClockVisibility) so all persistence
  paths (DB columns + localStorage) stay identical — no migration.
- home.tsx topbar now renders only inbox link, single notification
  bell (kept as nav link), Settings gear, and logout.
- New i18n keys under `settingsPanel.*` in ar.json + en.json.
- Old ClockStylePicker / NotificationSettingsPicker / QuickMuteButton
  exports remain in their original files (unused) — kept to minimize
  diff scope; can be removed in a follow-up.
2026-05-13 14:46:50 +00:00
riyadhafraa 143ad9a29d Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project
can be cloned and run on any Linux VPS with `docker compose up`.

Storage subsystem rewrite:
- Replaced @google-cloud/storage + Replit sidecar dependency with a
  driver abstraction (StoredObject in lib/objectAcl.ts) and two
  implementations: LocalDriver (filesystem + HMAC-signed PUT route at
  /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint
  via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected
  by STORAGE_DRIVER / S3_ENDPOINT env vars.
- Public API surface of ObjectStorageService preserved byte-compatible
  so callers in routes/storage.ts and routes/executive-meetings.ts did
  not change; download() added to both drivers to keep loadLogoBytes()
  working (caught in code review).
- Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in
  test C) all pass against the new local driver. Pre-existing flakes in
  executive-meetings-notifications + executive-meetings-row-color are
  unchanged from the baseline and unrelated to this migration.

Infrastructure:
- Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses
  the official Playwright base image so PDF rendering works in-container;
  web stage is nginx serving the Vite SPA bundle.
- docker-compose.yml: postgres + minio + minio-init (creates buckets) +
  api + web + one-shot migrate runner. Healthchecks on every long-lived
  service.
- docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket
  upgrade ordering.
- .env.example: every runtime env var documented with comments.
- README.md replaces replit.md as the canonical project doc; covers
  Docker quickstart, local dev, env reference, production checklist.
- MIGRATION_REPORT.md: file-by-file diff of what changed and why.

Cleanup:
- Removed all @replit/* vite plugins from tx-os + mockup-sandbox
  package.json + vite.config.ts + pnpm-workspace.yaml catalog.
- Removed @google-cloud/storage and google-auth-library from api-server.
- Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and
  *.tsbuildinfo build artefacts, scripts/post-merge.sh.
- Stripped Replit references from threat_model.md (now describes the
  self-hosted topology).
- New comprehensive .gitignore: Replit configs (.replit, replit.nix,
  replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/,
  .upm/), local storage/, .env*, build artefacts. .replit and replit.nix
  remain on disk (sandbox-protected) but will not ship to GitHub.
- scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD
  from env and throws in production if either is unset.

Drift from plan: replit.md was deleted (per off-Replit scope) so its
"do-not-touch files" preference list is moot. .replit/replit.nix kept on
disk only because they are sandbox-protected from edit/delete in this
environment, but they are git-ignored so they will not appear in a
fresh clone.
2026-05-13 14:08:11 +00:00
riyadhafraa d2b4f82736 Task #521: shorten meetings page URL to /meetings
- artifacts/tx-os/src/App.tsx: serve the schedule at /meetings; add an
  ExecutiveMeetingsRedirect that rewrites /executive-meetings (and any
  /executive-meetings/* sub-path) to /meetings while preserving the
  query string and hash, so existing PDFs, emails, and bookmarks keep
  working with one transparent hop.
- scripts/src/seed.ts: seed the apps row with route="/meetings" and
  update the expectedBuiltinRoutes drift guard to match. Add an
  idempotent UPDATE-by-slug after the apps insert so already-deployed
  rows (including any drift like /executive-meetings or /mms) get
  reconciled to /meetings on the next seed — safe because the slug is
  in BUILTIN_APP_SLUGS, meaning the SPA owns the route.
- artifacts/tx-os/tests/meetings-route-redirect.spec.mjs: new spec
  verifying that /meetings loads, /executive-meetings?date=...#... is
  redirected with query+hash intact, and /api/apps now reports
  route="/meetings" for the executive-meetings slug.

Out of scope (intentionally left untouched per task): API paths under
/api/executive-meetings/*, React Query keys, DB tables, the apps slug
"executive-meetings", page/component file names, the "Meetings" display
name, and the executive_meetings:* permission name.
2026-05-13 07:06:14 +00:00
riyadhafraa bfbf59cb20 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:52:11 +00:00
riyadhafraa 8b3c15ba21 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:50:45 +00:00
riyadhafraa 4a48614e43 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:49:20 +00:00
riyadhafraa 9d60b35955 Task #520: skip send-note confirm when only one recipient
The send-note composer always showed an AlertDialog before posting,
even for the common one-on-one case. The dialog now only appears when
two or more recipients are picked.

Implementation (artifacts/tx-os/src/pages/notes.tsx):
- Extracted shared `performSend()` from `confirmSubmit` so both the
  direct-send and confirm-then-send paths reuse the same mutation,
  success toast + composer close, and error toast.
- `requestSubmit()` now branches: 0 picked = no-op, 1 picked = call
  performSend() directly, 2+ picked = open the existing AlertDialog.
- `confirmSubmit()` (the AlertDialog action) delegates to performSend.
- No changes to API, recipient picker, AR/EN strings, or any other
  AlertDialog (bulk archive/delete left untouched).

Tests:
- Updated artifacts/tx-os/tests/notes-inbox.spec.mjs (sends to a
  single recipient): no longer expects the confirm dialog; asserts
  /api/notes/:id/send fires directly and the composer closes.
- New artifacts/tx-os/tests/notes-send-confirm-threshold.spec.mjs:
  - "single-recipient send skips the confirm dialog" (passes)
  - "multi-recipient send still requires the confirm dialog" (passes)

Validation: tx-os tsc clean; both threshold specs pass locally;
architect review APPROVED.
2026-05-13 06:47:44 +00:00
riyadhafraa 6a01ce852a Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required when an external mode is chosen.
- Internal route field is hidden entirely when an external mode is
  selected, and locked (readOnly + lock hint) when editing a built-in
  app. Slug input is also locked (readOnly) for built-in apps so the
  built-in identity cannot drift via the form.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug. Exposed via subpath export
  `@workspace/db/built-in-apps`; the file has zero imports so the
  browser bundle uses it without pulling in `pg`. tx-os now imports it
  directly — duplicate FE constant removed.
- Built-in slug list: services, notifications, admin, notes,
  my-orders, orders-incoming, executive-meetings (everything with a
  hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar /
  documents are seeded but admin-defined and remain editable.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.
- PATCH /apps/:id ALSO rejects slug changes when the previous slug is
  built-in (code='builtin_slug_locked'). UpdateAppBody zod schema
  intentionally omits slug, so we inspect req.body.slug raw before zod
  stripping. Closes the 2-step bypass: rename slug (allowed) → change
  route (now previous.slug looks non-built-in, allowed).
- POST /apps and PATCH /apps/:id reject externalUrl values that are
  not http:// or https:// (code='invalid_external_url'). Prevents
  shipping javascript:/data:/file: payloads tenant-wide via launcher.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- scripts/src/seed.ts: drift guard throws if a seeded built-in slug
  uses a route that does not match the hardcoded SPA route.

Tests:
- New API test apps-builtin-route-lock.test.mjs (6/6 pass): reject
  built-in route change, allow non-route built-in updates, allow
  non-builtin route changes, reject built-in slug change (anti-bypass),
  reject non-http(s) externalUrl scheme + accept https, allow built-in
  same-route no-op.
- New Playwright E2E admin-app-image-external-embedded.spec.mjs
  (passes): launcher renders custom image_url, external_tab opens
  external URL via window.open, external_iframe navigates to
  /embedded/:id and renders <iframe src=externalUrl>.

Out of scope (pre-existing, not introduced here):
- Failing tests in executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts.
2026-05-12 12:37:34 +00:00
riyadhafraa e7948012f4 Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required when an external mode is chosen.
  Internal route field is hidden entirely when an external mode is
  selected, and locked (readOnly + lock hint) when editing a built-in
  app whose path is hardcoded in the SPA.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug. Exposed via subpath export
  `@workspace/db/built-in-apps`; the file has zero imports so the
  browser bundle can use it without pulling in `pg`. tx-os now imports
  it directly — duplicate FE constant removed.
- Built-in slug list: services, notifications, admin, notes,
  my-orders, orders-incoming, executive-meetings (everything with a
  hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar /
  documents are seeded but admin-defined and remain editable.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- scripts/src/seed.ts: drift guard throws if a seeded built-in slug
  uses a route that does not match the hardcoded SPA route.

Tests:
- New API test apps-builtin-route-lock.test.mjs (4/4 pass): reject
  built-in route change, allow non-route built-in updates, allow
  non-builtin route changes, allow built-in same-route no-op.
- New Playwright E2E admin-app-image-external-embedded.spec.mjs
  (passes): launcher renders custom image_url, external_tab opens
  external URL via window.open, external_iframe navigates to
  /embedded/:id and renders <iframe src=externalUrl>.

Out of scope (pre-existing, not introduced here):
- 3 failing tests in executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts.
2026-05-12 12:31:43 +00:00
riyadhafraa a794f92e61 Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required by the form when an external mode
  is chosen.
- Route field is locked (readOnly + lock hint) when editing a built-in
  app, since those slugs are hardcoded in the SPA router.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug, re-exported from lib/db.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- New tests in apps-builtin-route-lock.test.mjs (4/4 pass) covering
  reject built-in route change, allow non-route built-in updates,
  allow non-builtin route changes, allow built-in same-route no-op.

Notes / drift:
- BUILTIN_APP_SLUGS is duplicated inline in admin.tsx
  (BUILTIN_APP_SLUGS_FE) because the browser bundle cannot import
  @workspace/db (pulls pg). Comment points at the canonical source;
  drift risk filed as a follow-up.
- Pre-existing failures unrelated to this task: 3 tests in
  executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts. Out of scope.
2026-05-12 12:23:38 +00:00
riyadhafraa f62969dc53 Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to
/notes?thread=X&reply=1, yanking the user out of whatever page they
were on (Tasks, Calendar, etc.). Now Reply opens an inline composer
inside the popup card itself.

Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx):
- New composerOpen / replyText / justSent state, reset whenever the
  popup head rotates so a half-typed reply never leaks across payloads.
- handleReply now opens the inline composer and focuses the textarea
  (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to
  successful send so Cancel keeps the note unread.
- handleSubmitReply uses the existing useReplyToNote hook. Resolves
  recipientUserId from replyPayload.replier.id for the reply variant
  (owner replying back), undefined for the note variant (recipient
  replying — server infers the original sender).
- On success: brief on-card "Reply sent" confirmation (aria-live),
  then auto-dismiss after 700ms. On failure: destructive toast with
  the textarea preserved.
- Composer textarea: Enter (or Cmd/Ctrl+Enter) sends, Shift+Enter
  inserts a newline. Esc cancels the composer and is handled by the
  popup's global keydown listener so it works whether focus is in the
  textarea or on the Send/Cancel buttons. dir="auto" so the user's
  reply renders RTL/LTR per its own characters.
- pointerdown/click stopPropagation on the composer wrapper so typing
  doesn't accidentally drag the card.

Locale keys added to ar.json + en.json under notes.popup:
inlineReplyPlaceholder, sendReply, cancelReply, replySending,
replySent, replyFailed.

Pre-existing failing tests (groups-crud, executive-meetings-
notifications, executive-meetings-postpone-race) are unrelated to
this change.
2026-05-12 11:58:37 +00:00
riyadhafraa a55fb16c4c Task #515: Reply to a note from the popup without leaving the page
The floating "new note" popup's Reply button used to navigate to
/notes?thread=X&reply=1, yanking the user out of whatever page they
were on (Tasks, Calendar, etc.). Now Reply opens an inline composer
inside the popup card itself.

Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx):
- New composerOpen / replyText / justSent state, reset whenever the
  popup head rotates so a half-typed reply never leaks across payloads.
- handleReply now opens the inline composer and focuses the textarea
  (rAF) instead of routing. Acknowledge (mark-as-read) is deferred to
  successful send so Cancel keeps the note unread.
- handleSubmitReply uses the existing useReplyToNote hook. Resolves
  recipientUserId from replyPayload.replier.id for the reply variant
  (owner replying back), undefined for the note variant (recipient
  replying — server infers the original sender).
- On success: brief on-card "Reply sent" confirmation (aria-live),
  then auto-dismiss after 700ms. On failure: destructive toast with
  the textarea preserved.
- Composer textarea: Cmd/Ctrl+Enter to send, plain Enter for newline,
  Esc to cancel (stops propagation so the global Esc listener doesn't
  dismiss the whole popup with an unsent reply). dir="auto" so the
  user's reply renders RTL/LTR per its own characters.
- pointerdown/click stopPropagation on the composer wrapper so typing
  doesn't accidentally drag the card.

Locale keys added to ar.json + en.json under notes.popup:
inlineReplyPlaceholder, sendReply, cancelReply, replySending,
replySent, replyFailed.

Pre-existing failing tests (groups-crud, executive-meetings-
notifications, executive-meetings-postpone-race) are unrelated to
this change.
2026-05-12 11:57:08 +00:00
riyadhafraa b7b8c395f4 Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Switched the folders+content row in
  `artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid:
  `grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in
  col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is
  shown), content in col 2 row 2. Result: composer's top edge aligns
  with the rail's top edge on desktop and the empty gap is gone.
- On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the
  exact original mobile reading order: composer → rail → content.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root `<aside>`, so the page can pass grid placement
  classes.
- Hidden-composer views (Inbox / Sent / Archived / Shared-with-me)
  collapse cleanly via conditional `md:row-span-2` on the rail and
  conditional `md:row-start-1 / md:row-start-2` on the content
  column — desktop becomes a simple two-cell row and mobile stays
  rail → content, matching the original behavior.
- Grid-cols template, `md:col-start-2` on composer, and
  `md:col-start-2`/`md:row-start-*` on the content column are all
  GATED on `showFolders`. In views where the folders rail is hidden
  (Received/Inbox, Sent), the wrapper is a single-column grid with
  no left gutter and content takes the full width — preserving the
  original full-width layout for those views.

Behavior preserved:
- `showTopComposer` gating unchanged.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing internal classes unchanged; new className
  prop is purely additive.
- Mobile reading order in active/non-shared view: composer → rail →
  content (same as before this task, when composer was a standalone
  block above the row).
- Mobile reading order in hidden-composer views: rail → content
  (unchanged).

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:31:37 +00:00
riyadhafraa cb6b067386 Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Switched the folders+content row in
  `artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid:
  `grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in
  col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is
  shown), content in col 2 row 2. Result: composer's top edge aligns
  with the rail's top edge on desktop and the empty gap is gone.
- On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the
  exact original mobile reading order: composer → rail → content.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root `<aside>`, so the page can pass grid placement
  classes.
- Hidden-composer views (Inbox / Sent / Archived / Shared-with-me)
  collapse cleanly via conditional `md:row-span-2` on the rail and
  conditional `md:row-start-1 / md:row-start-2` on the content
  column — desktop becomes a simple two-cell row and mobile stays
  rail → content, matching the original behavior.

Behavior preserved:
- `showTopComposer` gating unchanged.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing internal classes unchanged; new className
  prop is purely additive.
- Mobile reading order in active/non-shared view: composer → rail →
  content (same as before this task, when composer was a standalone
  block above the row).
- Mobile reading order in hidden-composer views: rail → content
  (unchanged).

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:30:09 +00:00
riyadhafraa d1875141aa Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Moved the top composer out of its standalone full-width block and
  into the left content column (`flex-1 min-w-0`) of the existing
  folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both
  now share the same row, so the composer's top edge aligns with the
  rail's top edge on desktop.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root <aside>, so the page can pass `order-*` classes.
- On narrow widths (`flex-col`) the composer column gets
  `order-1 md:order-2` and the rail gets `order-2 md:order-1` ONLY
  when `showTopComposer` is true. In hidden-composer views (Inbox /
  Sent / Archived / Shared-with-me) no order classes are applied, so
  the rail/content stacking order on mobile stays exactly as it was
  before this task. Result: composer stacks ABOVE the rail on mobile
  in My Notes (preserving the original mobile reading order) and
  sits beside it on md+, with zero behavior change in the other
  views.

Behavior preserved:
- `showTopComposer` gating unchanged — still hidden on Inbox / Sent /
  Archived / Shared-with-me.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing width/scroll classes unchanged; new className
  is purely additive.
- Stale comment about composer being "above the folders area" updated
  to describe the new in-column placement and the order-* mobile
  behavior.

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:27:10 +00:00
riyadhafraa 944a894a88 Task #514: Align note composer with folders rail
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.

Change:
- Moved the top composer out of its standalone full-width block and
  into the left content column (`flex-1 min-w-0`) of the existing
  folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both
  now share the same row, so the composer's top edge aligns with the
  rail's top edge on desktop.
- Added a `className?: string` prop to `FoldersRail`
  (`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
  to its root <aside>, so the page can pass `order-*` classes.
- On narrow widths (`flex-col`) the composer column gets
  `order-1 md:order-2` and the rail gets `order-2 md:order-1`. Result:
  composer stacks ABOVE the rail on mobile (preserving the original
  mobile reading order) and sits beside it on md+.

Behavior preserved:
- `showTopComposer` gating unchanged — still hidden on Inbox / Sent /
  Archived / Shared-with-me.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing width/scroll classes unchanged; new className
  is purely additive.
- Stale comment about composer being "above the folders area" updated
  to describe the new in-column placement and the order-* mobile
  behavior.

Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.

No deviations from the plan.
2026-05-12 11:25:45 +00:00
riyadhafraa 037d530471 Task #514: Align note composer with folders rail
Original task: On the Notes page, the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below it. The result was a visible
vertical mismatch — composer high, rail noticeably lower with empty
space between them.

Change: Moved the top composer out of its standalone full-width block
and into the left content column (`flex-1 min-w-0`) at the top of the
existing row that already hosts the FoldersRail. Both now share the
same row, so the composer's top edge aligns with the rail's top edge.

Details:
- Single edit in `artifacts/tx-os/src/pages/notes.tsx`.
- The composer's `data-testid="notes-top-composer"` is preserved, so
  any existing E2E selectors continue to work.
- `showTopComposer` gating (hide on Inbox/Sent/Archived/Shared-with-me)
  is unchanged — only the position of the rendered block moved.
- Removed the old wrapper's `px-4 pt-1 max-w-6xl w-full mx-auto`
  (those paddings/centering are now provided by the parent row), and
  added a `mb-4` for breathing room between composer and the content
  below it inside the column.
- Responsive behavior preserved: the parent row is
  `flex-col md:flex-row`, so on narrow widths the composer still
  stacks above the folders area cleanly with no horizontal overflow.

Verification: pnpm tsc --noEmit on tx-os passes; no test files
reference the composer block by structural selectors that the move
would break.

No deviations from the plan.
2026-05-12 11:22:47 +00:00