- New shared component artifacts/tx-os/src/components/attendee-table-editor.tsx:
AttendeeTableEditor (# | name | position | side select | remove), plus
syncAttendeeRows/emptyAttendeeRow helpers and MAX_ATTENDEE_ROWS=50 cap.
- Internal booking dialog (protocol.tsx): replaced the two side-by-side
BookingAttendeeColumn lists with the single numbered table; each row now
carries its own side. Edit mode prefills existing keyAttendees then pads
to attendeeCount. useEffect syncs row count with the attendeeCount field:
growing adds empty rows, shrinking removes only empty trailing rows (never
filled ones), min 1 row.
- Public form (protocol-request.tsx): same conversion (AttendeeColumn removed,
Arabic labels passed directly since the page is forced-RTL Arabic).
- Submission payload unchanged: only rows with non-empty name sent, same
keyAttendees {name, position, side} shape — no API/schema changes.
- i18n: added protocol.bookings.form.side ("الجهة"/"Side") in ar.json/en.json.
- Removed dead code (BookingAttendeeColumn, AttendeeColumn, AttendeeRow type,
unused Plus/X imports in protocol-request).
- Verified: tsc --noEmit clean; page serves 200; architect review PASS.
- Follow-up proposed: #719 UI test for count sync/prefill/submit.
Problem: In the public room-booking form (protocol-request.tsx), the date
picker calendar showed English weekday headers and a scrambled day grid under
RTL. Root cause: the shared Calendar (react-day-picker v9) was rendered without
`locale` or `dir`, so weekdays stayed English and its flex week rows reversed
visually under CSS RTL.
Changes (artifacts/tx-os/src/components/ui/calendar.tsx):
- Pass date-fns Arabic locale (`ar`) + `dir="rtl"` when Arabic is active, else
`enUS` + `dir="ltr"`; placed before `{...props}` so callers can still override.
- Added `formatCaption` and `formatDay` formatters (alongside existing
`formatMonthDropdown`) using Intl `numberingSystem: "latn"` to keep Western
digits, matching the app-wide AR_LATN convention (Arabic locale would
otherwise emit Arabic-Indic numerals).
- `isArabic()` now resolves from `document.documentElement.lang`/`dir` first,
falling back to `i18n.language`.
Deviation from plan: after code review, changed language detection from
i18n.language-only to DOM-first. Reason: the booking page forces Arabic by
setting documentElement.lang/dir without changing i18n.language, so a guest with
saved English would otherwise get an English/LTR calendar on that page.
Out of scope (per user): no time-picker or من/إلى changes.
Verified: `tsc --noEmit` passes for @workspace/tx-os. English calendar
unaffected (default enUS/LTR).
Introduce a custom DateTimePicker component for improved date and time selection in the protocol request form, replacing native inputs with an Arabic-localized, RTL-compatible interface.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8b01db58-6cde-464b-bd7d-46bd031e467f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/OsTuUKE
Replit-Helium-Checkpoint-Created: true
User asked for three things in the executive-meetings module:
1. Remove the "daily number" input from the add/edit dialog — numbering
is auto-assigned by the server already, so the field was just noise.
2. Add an "اجتماع خارجي" (external meeting) toggle. When ON, the row
is force-tinted red in the schedule grid.
3. External meetings must be excluded from the auto-postpone flow:
the alert "Postpone" button is disabled, and the cascade shift
skips them as followers.
Changes
- lib/db/src/schema/executive-meetings.ts
+ `is_external boolean not null default false` column. Pushed via
drizzle-kit (no destructive migration; defaults backfill).
- artifacts/api-server/src/routes/executive-meetings.ts
+ `isExternal` added to base zod fields, POST insert, PATCH update.
+ POST/PATCH force `rowColor='red'` when `isExternal=true`.
+ POST /:id/postpone-minutes rejects external meetings with
HTTP 409 + code `external_meeting_no_auto_postpone`.
+ `computeCascadeShift` filters out external followers (preview +
writer stay in lockstep, as the comment promises).
- artifacts/tx-os/src/pages/executive-meetings.tsx
+ Meeting type + MeetingFormState gain `isExternal`; dropped the
`dailyNumber` field from the form state entirely.
+ `emptyMeetingForm` / `openEdit` updated; ManageSection.save() and
the inline ScheduleSection save body now send `isExternal` and
never send `dailyNumber`.
+ MeetingFormDialog: removed the daily-number FormRow and added a
Switch-based external-meeting FormRow with localized hint.
+ `rowColors` memo coerces to "red" whenever `m.isExternal`.
+ Audit-diff "interesting" list now includes `isExternal`.
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
+ Meeting type gains optional `isExternal`.
+ "Postpone" button disabled + tooltip when meeting is external.
- locales: added `field.isExternal`, `field.isExternalHint`,
`field.isExternalOn/Off`, and `alert.externalCannotPostpone` in
both ar.json and en.json. Existing dailyNumber keys kept (still
referenced by the manage-table column header / cell display).
Post-review hardening (from architect pass)
- PATCH now coerces rowColor → 'red' for any PATCH that touches a row
that is (or becomes) external. Prevents PDF/export paths that read
raw rowColor from showing a non-red tint for externals.
- Schedule row quick-actions popover: the "Postpone" button is now
disabled with a localized tooltip when the meeting is external,
matching the upcoming-meeting alert behavior.
- PostponeDialog.handleErr maps the 409 code
`external_meeting_no_auto_postpone` to the localized
`alert.externalCannotPostpone` toast so users see a real reason
instead of the raw English error string.
Notes
- The `dailyNumber` column stays in DB (heavily used for slot
persistence and the unique index); only the form input was removed.
Server's `nextDailyNumber()` already auto-assigns when the field is
absent on POST, and PATCH leaves it untouched when absent.
- Two pre-existing TS errors remain in api-server (`isHighlighted`
type-inference quirk on `z.union(...).transform(...)` and a
font_settings comparison) — not in scope for this task.
- Tests not added; verification by typecheck on changed surfaces and
workflow-restart smoke. No e2e harness was wired.
User-reported regression from #630: opening "Send note" on iPad PWA
showed the recipient list pushed to the left side of the screen,
narrower than designed, with the dialog title + search input clipped
off the top. Dismissing the keyboard restored normal centered layout.
Root cause: the `keyboardFallback` branch (iPad PWA, where
visualViewport doesn't update) set `top: "6vh"` in *layout viewport*
coordinates. On iPad PWA Safari, opening the keyboard scrolls the
page down behind the keyboard, so layout-viewport `6vh` ends up
above the visible visual viewport — clipping the top half of the
dialog. Separately, the inline `transform` replaced Tailwind's
translate-x/y on Radix's content, and without an explicit `left:50%`
the animation/RTL combination shifted the box left.
Fix (artifacts/tx-os/src/components/notes/send-note-dialog.tsx):
- `keyboardFallback`: `top: calc(${vv.offsetTop || 0}px + 6vh)` so the
anchor follows the visible visual viewport when Safari scrolls the
page behind the keyboard. Add explicit `left: 50%`. Add
`overflowY: auto` so if dialog content exceeds 50dvh, the dialog
scrolls internally instead of clipping the header.
- `keyboardActive` branch: also add explicit `left: 50%` defensively
for the same animation/RTL reason (no behavior change on devices
where it was already working, but eliminates the asymmetry).
No changes to the shared `dialog.tsx`, composer, or executive-meetings
dialog — scope strictly limited to the regression site.
TypeScript: clean. Pre-existing errors in api-server/src/routes/push.ts
unrelated to this diff.
Problem: opening Send Note on iPad, tapping the search field opened the
soft keyboard which covered the recipients list and the Send button —
the dialog stayed centered on the full layout viewport.
Fix (artifacts/tx-os/src/components/notes/send-note-dialog.tsx):
Two-tier keyboard detection:
1. Primary — `vv.keyboardInset > 0` from useVisualViewport. Works on
desktop touch laptops, iPad Safari (regular tab), Android Chrome.
When true, override DialogContent style with
top = vv.offsetTop + vv.height/2 − 48 (QuickType inset)
transform: translate(-50%, -50%)
so the dialog recenters around the visible viewport region.
2. Fallback — `searchFocused && matchMedia('(pointer: coarse)')`.
Only used when visualViewport does NOT report an inset, which is
the iPad PWA (Add to Home Screen) case explicitly called out in
components/ui/dialog.tsx comment. Top-anchors at 6vh with
maxHeight 50dvh so the dialog never crosses the screen midpoint.
3. Neither — pass NO style at all so Radix's default centered layout
is fully restored (no leftover transition or top).
Touch-only fallback gating (matchMedia pointer:coarse) prevents the
earlier desktop-regression flagged by code review — focusing the
search on a desktop never repositions the dialog.
Other changes:
- onFocus/onBlur on the search input feed the fallback signal.
- Blur is debounced 150ms so tapping a recipient row mid-blur doesn't
recenter the dialog out from under the finger.
- useEffect cleanup clears pending blur timeout on unmount.
Verification: tsc clean. Architect approved the touch-gated fallback;
follow-up review reconciled the diff with the spec's primary
visualViewport-driven path.
Problem: opening Send Note on iPad PWA, tapping the search field popped
up the soft keyboard which covered the entire people list and the Send
button. The previous fix relied on visualViewport to shrink the list,
but iPad PWAs don't reliably resize visualViewport when the keyboard
opens (see the note in components/ui/dialog.tsx referencing the
reverted #622–#624 global attempts).
Fix (artifacts/tx-os/src/components/notes/send-note-dialog.tsx):
- Track search-input focus directly — the most reliable "keyboard is
about to open" signal on touch devices, independent of visualViewport.
- Gate the override behind `matchMedia('(pointer: coarse)')` so it
only fires on touch-primary devices (iPad/iPhone/Android) and never
on desktop or trackpad laptops — addressing the architect's
cross-platform regression flag.
- When the gate is active, override DialogContent's Radix center
anchor (top:50%; translate(-50%,-50%)) with `top:6vh;
translate(-50%,0); maxHeight:50dvh`. The dialog grows downward from
a fixed top edge so it always fits in the upper half of the screen,
leaving the lower half free for the keyboard.
- Cap `listMaxHeight` to ~50% of innerHeight in the same gated
condition, as a backstop where visualViewport stays full-height.
- Debounce blur by 150ms so a tap on a recipient row doesn't lose
the row mid-tap to a center-jump animation; clear the timeout on
unmount.
- Added 120ms ease transition on top/transform so the layout swap
doesn't snap.
Deviations: original plan used vv.offsetTop+vv.height/2 to reposition,
but that depends on the same unreliable visualViewport. Switched to
a focus + coarse-pointer driven approach which works on iPad PWA.
Verification: tsc clean. Architect re-review pending after the touch
gate was added (first review flagged the missing platform guard,
which this revision addresses directly).
Modify dialog component to use window.innerWidth for width and position fixed, ensuring consistent display across iOS PWA environments by anchoring to the layout viewport.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b5415cc4-9462-49ea-bda1-5d40d587d3d1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qrzw3bH
Replit-Helium-Checkpoint-Created: true
Adjust dialog positioning logic to use visual viewport dimensions for precise placement and sizing on iOS, resolving display issues caused by layout viewport discrepancies with the soft keyboard.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f92aad5a-65d6-41c4-a2d9-0806c97f1464
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qrzw3bH
Replit-Helium-Checkpoint-Created: true
Add logic to `send-note-dialog.tsx` to dynamically adjust the recipients list's max-height based on the visual viewport, ensuring visibility when the iPad keyboard is active.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3443b1ba-ba66-4f71-852a-129ee45731d0
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/IO8TMSC
Replit-Helium-Checkpoint-Created: true
Update styling for date and time inputs in the reschedule dialog to improve mobile responsiveness and visual appearance, including stacking elements on smaller screens and adjusting input sizes.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: bffeee36-759c-495f-8ffe-bff53829d905
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/IO8TMSC
Replit-Helium-Checkpoint-Created: true
Adjust styling for mobile responsiveness in the notes header, improve contrast for completed checklist items, and refine the layout of the executive meetings form dialog.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ce2e0a68-adc1-43df-936b-f4a7772ac303
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/IO8TMSC
Replit-Helium-Checkpoint-Created: true
Refactor the `PostponeDialog` component to enhance its visual design and user experience, including updated styling for dialog content, headers, tab strips, date/time inputs, and minute chips.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 339e1165-8b3c-4554-ae1b-989743df7fbc
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/IO8TMSC
Replit-Helium-Checkpoint-Created: true
The per-app hidden feature shipped in #609 was rejected — user wanted one
toggle that hides/shows the entire bottom AppDock bar, leaving Home apps
untouched.
Removed:
- lib/db/src/schema/user-hidden-apps.ts (and index.ts export); table
dropped via `pnpm --filter @workspace/db run push`
- GET /me/apps and PUT /me/apps/:appId/hidden routes
- getHiddenAppIdsForUser + getVisibleNonHiddenAppsForUser helpers
- MyApp + UpdateMyAppHiddenBody OpenAPI schemas
- MyAppsBody settings UI + related i18n keys
- Regenerated api-client-react + api-zod from the trimmed spec
- Reverted GET /apps back to getVisibleAppsForUser
Added:
- artifacts/tx-os/src/hooks/use-dock-visible.ts — localStorage-backed
preference with a custom window event for in-tab sync and the native
`storage` event for cross-tab sync. Default = true.
- DockBody in settings-panel: single ToggleRow under a new "App dock"
section ("settingsPanel.section.dock" / "settingsPanel.dock.show")
- AppDock now returns null when the preference is off and clears
`--app-dock-height` so page padding doesn't stay reserved.
Code review: PASS (architect). No remaining references to the removed
infra. Typecheck shows only pre-existing errors in executive-meetings.ts
and push.ts unrelated to this change.
Follow-up: publish to Gitea + redeploy on Mac (proposed as a follow-up
task).
- New `user_hidden_apps` table (userId+appId composite PK, cascade) in
lib/db/src/schema/user-hidden-apps.ts; registered in schema index;
pushed to dev DB via drizzle-kit.
- Backend (artifacts/api-server/src/routes/apps.ts):
- GET /me/apps — returns every globally-active app visible to the
user with a `hidden` flag.
- PUT /me/apps/:appId/hidden — toggles a row in user_hidden_apps,
gated by getVisibleAppsForUser + isActive so a user can't toggle
apps they can't reach.
- GET /apps (Home/Dock) now uses getVisibleNonHiddenAppsForUser so
hidden apps disappear immediately.
- lib/appsVisibility.ts: added getHiddenAppIdsForUser and
getVisibleNonHiddenAppsForUser helpers.
- OpenAPI: added /me/apps + /me/apps/{appId}/hidden, MyApp and
UpdateMyAppHiddenBody schemas; regenerated api-zod + api-client-react.
- Frontend (settings-panel.tsx): added "My apps" accordion section as
first GroupItem with MyAppsBody — Switch per app, optimistic update,
invalidates getListMyAppsQueryKey + getListAppsQueryKey so Home/Dock
refresh without reload.
- Translations: added settingsPanel.section.myApps + settingsPanel.myApps
in ar.json + en.json.
- Code review fix: /me/apps and PUT gating filter isActive even for
admins, so inactive apps don't appear in the Settings list.
- Proposed follow-up #611 (Playwright test that hidden apps disappear
from Home and Dock).
Problem: The floating AppDock (position: fixed at bottom-2/3) overlapped
the last row of long pages on mobile — e.g. meeting #7 on the Meetings
page was hidden behind the dock with no way to scroll it into view.
Fix:
- AppDock measures its rendered outer-bottom extent (height + bottom
offset + safe-area inset) via getBoundingClientRect() and publishes
it as `--app-dock-height` on documentElement, with a small 8px gap
so the last row doesn't kiss the dock.
- Re-measures on ResizeObserver, window resize, and orientationchange
so the value stays correct across rotation, URL-bar collapse, and
dock content changes.
- Clears the variable on unmount and whenever the dock hides
(≤1 other app), so pages without a dock get no extra padding.
- Global rule in index.css: `body { padding-bottom: var(--app-dock-height, 0px) }`,
scoped to `@media not print` so PDF exports stay unaffected.
Files:
- artifacts/tx-os/src/components/app-dock.tsx
- artifacts/tx-os/src/index.css
No deviations from the task plan.
Problem: On phone-width viewports (~375-430px), the floating
FormattingToolbar in EditableCell rendered as a single row with
17+ controls (B/I/U + 7 color swatches + 3 align buttons + font +
size + Save/Cancel). The row was wider than the screen, so users
only saw the edges (X, ✓, two "default" dropdowns) and could not
reach Bold/Italic/Underline, color swatches, alignment, or font
controls. The horizontal clamp also relied on the table's
scroll-container bounds, which on mobile extend beyond the viewport
because the table is wider than the screen — so the clamp could
park the toolbar partially off-screen.
Fix in artifacts/tx-os/src/components/editable-cell.tsx:
- Toolbar container: `flex` → `flex flex-wrap` with `gap-x-1 gap-y-1`
and `max-w-[calc(100vw-16px)]` so it wraps onto multiple rows
whenever a single row would exceed the viewport. Bumped `py` to
`py-1` for breathing room between wrapped rows.
- Horizontal clamp: in addition to the scroll-container bounds,
also clamp against the viewport (`[8, window.innerWidth - tbWidth
- 8]`) so the toolbar always lands fully on-screen even when the
scroll container is wider than the screen.
Scope: visual / positioning only. No changes to TipTap config,
toolbar buttons, or save/cancel logic. Above-vs-below placement
(#581 iOS keyboard handling) is preserved.
Problem: In the "تأجيل الاجتماع" dialog's "إعادة الجدولة" tab, the two
<input type="time"> fields for Start and End rendered as iOS Safari's
native "pill" controls centered inside transparent, hairline-bordered
cells separated by only gap-3. On iPad the pills visually touched in
the middle of the row, so users perceived a single overlapping
control instead of two distinct inputs.
Fix in artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx:
- Increase the grid gap from gap-3 to gap-4 / sm:gap-6 so the two
cells never visually merge regardless of viewport width.
- Add `min-h-11 bg-background border-2` to each Input so each cell
has an explicit, clearly bounded box around the iOS native pill.
- Added an inline #606 comment explaining the iOS quirk so future
edits don't revert the layout.
Scope: layout-only change to the reschedule tab. No changes to
reschedule logic, validation, cascade prompt, or the postpone/cancel
tabs.
PWA + Web Push infra was already fully built (SW, VAPID auto-gen,
subscribe/unsubscribe API, sendPushToUser called from orders/meetings/
notes/replies, iOS-PWA detection hook). The user wasn't getting
lock-screen alerts because iPhone/iPad weren't installed as a PWA —
iOS only delivers Web Push from a Home Screen icon, not a Safari tab.
This commit polishes the iOS install path so the gap is obvious:
1. push-enable-prompt.tsx — Added a dedicated iOS install-steps card
variant that renders on iPhone/iPad Safari when running outside
standalone mode. Shows Share → Add to Home Screen → open from icon.
Independent 14-day dismiss memory from the regular Enable card.
2. notification-settings.tsx — PushToggleRow now detects iOS-non-
standalone and shows the unsupported_ios_safari hint inline with a
disabled toggle, instead of teasing an Enable affordance that always
fails.
3. use-push-subscription.ts — Exported isIosSafariNonStandalone() so
both surfaces share the detection logic (was private).
4. lib/push.ts (server) — Added info logs in loadVapid() for both the
env-key and disk-key paths, removed an accidental duplicate disk-read
block introduced during editing. Now the production redeploy check
is a one-liner: grep for "VAPID keys loaded" in the api logs.
5. ar.json / en.json — Added notifSettings.push.iosInstall.{title,desc,
step1,step2,step3} bilingual strings for the new card.
No DB migrations. No deps changed. tx-os tsc passes; api-server tsc
errors are pre-existing (routes/push.ts handler type, font_settings)
and not touched by this change.
Smoke test on real iPhone/iPad still owed (out-of-band — task step #4
is a manual verification on the user's devices after redeploy).
New shared component `artifacts/tx-os/src/components/app-dock.tsx`:
takes a `currentSlug` prop, pulls the apps list from the same
`useListApps()` query Home uses, filters out the current app and any
inactive ones, and renders a floating centered pill near the bottom
with one circular tinted button per remaining app. Reuses Home's
`resolveIcon(name)` pattern (Lucide string -> component, falls back to
Grid2X2) and Home's openApp logic (internal -> setLocation, external
_tab -> window.open, external_iframe -> /embedded/:id). Renders
nothing when fewer than 1 other apps are available. Hidden in print,
respects safe-area-inset-bottom, scrollable on narrow widths.
Mounted on every app page except Home:
- executive-meetings.tsx (currentSlug="executive-meetings", matches
seed slug; hidden when isFullscreen is true so the schedule fills
the screen unobstructed).
- services.tsx (currentSlug="services")
- notes.tsx (currentSlug="notes")
- orders-incoming.tsx (currentSlug="orders-incoming" — not a built-in
app slug, so all apps show, which is the right UX since incoming
inbox is reached via the topbar, not the home grid).
- admin.tsx (currentSlug="admin")
Removed the two gear/back-arrow blocks added by #596 in
executive-meetings.tsx (test ids em-president-settings,
em-president-back-to-schedule) — the dock now provides cross-app
navigation including Admin/Settings. The L902 force-schedule effect
that snaps the president back to the schedule section outside of
schedule|settings is left in place; it still gates the hidden
Manage / Audit tabs.
Verified `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
No backend, no new translations, Home untouched.
New shared component `artifacts/tx-os/src/components/app-dock.tsx`:
takes a `currentSlug` prop, pulls the apps list from the same
`useListApps()` query Home uses, filters out the current app and any
inactive ones, and renders a floating centered pill near the bottom
with one circular tinted button per remaining app. Reuses Home's
`resolveIcon(name)` pattern (Lucide string -> component, falls back to
Grid2X2) and Home's openApp logic (internal -> setLocation, external
_tab -> window.open, external_iframe -> /embedded/:id). Renders
nothing when fewer than 1 other apps are available. Hidden in print,
respects safe-area-inset-bottom, scrollable on narrow widths.
Mounted on every app page except Home:
- executive-meetings.tsx (currentSlug="executive-meetings", matches
seed slug; hidden when isFullscreen is true so the schedule fills
the screen unobstructed).
- services.tsx (currentSlug="services")
- notes.tsx (currentSlug="notes")
- orders-incoming.tsx (currentSlug="orders-incoming" — not a built-in
app slug, so all apps show, which is the right UX since incoming
inbox is reached via the topbar, not the home grid).
- admin.tsx (currentSlug="admin")
Removed the two gear/back-arrow blocks added by #596 in
executive-meetings.tsx (test ids em-president-settings,
em-president-back-to-schedule) — the dock now provides cross-app
navigation including Admin/Settings. The L902 force-schedule effect
that snaps the president back to the schedule section outside of
schedule|settings is left in place; it still gates the hidden
Manage / Audit tabs.
Verified `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
No backend, no new translations, Home untouched.
The user reported: when a 5-min meeting reminder fires and they hit
"Postpone 5 min", the second reminder appears at the new time but
**without sound** — and the only way to get the sound back is to leave
the app and come back (full tab reload).
Root cause: `UpcomingMeetingAlert` keeps a `playedRef: Set<number>` of
meeting ids that have already chimed, so the 30s polling/refetch
doesn't replay the chime over and over. The alert is mounted globally
in `App.tsx` and never unmounts during in-app navigation, so the Set
lives for the whole session. Postpone changes the meeting's
`startTime` but keeps the same `id`, so the second alert hit the
"already chimed" guard and stayed silent. Only a real tab reload
cleared the Set, which is exactly the workaround the user discovered.
Fix: change the dedupe key from `meeting.id` (number) to
`${meeting.id}:${startTime}` (string). Every postpone moves
startTime, so each post-postpone alert is treated as a fresh
instance and chimes again. The 3s throttle inside
`NotificationPlayer` still protects against burst-replay within the
same alert instance.
Untouched:
- `shownRef` — still keyed by id, because the server-side "shown"
audit row is one-per-meeting-per-user by design.
- `NotificationPlayer`, iOS unlock path, vibration, throttle.
- Postpone API surface and `PostponeDialog`.
Verified: `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
The yellow side tab on /executive-meetings previously opened a bespoke
textarea panel I added in #590. The user pointed out (screenshot) that
it should be the exact same composer card they use on /notes — yellow
background, color picker, label menu, checklist toggle, "حفظ" Save,
paper-plane Send.
Changes:
- Extracted Composer + its inline helpers (ChecklistEditor, KindToggle,
ColorPicker, LabelMenu, newItemId) out of pages/notes.tsx into a new
shared module at components/notes/composer.tsx.
- Extracted SendNoteDialog out of pages/notes.tsx into a new shared
module at components/notes/send-note-dialog.tsx.
- pages/notes.tsx now imports both modules; behavior of the notes page
is unchanged.
- pages/executive-meetings.tsx: deleted InlineQuickNotePanel. The
schedule view now renders <Composer folderSelection={{kind:"all"}}>
inside the yellow side-tab toggle, plus <SendNoteDialog> for the
paper-plane Send flow. A new quickNoteTrigger counter is bumped each
time the tab opens so the shared Composer auto-expands and focuses
its textarea. A small X in the corner dismisses the panel; Save just
collapses the composer (matches /notes UX), Send saves then opens
the recipient picker.
- Notes created from the meetings panel land as unfiled notes in the
user's notes (folder "all"), visible immediately in /notes.
- Composer gained an optional onClose prop fired from its reset()
path (save / outside-click save / send success / empty reset).
/notes doesn't pass it (composer stays mounted, just collapses).
The meetings page wires it to setQuickNoteOpen(false) so the panel
dismisses and the yellow side tab returns after every save path,
not just the X / Send paths. Addresses code-review feedback.
Notes:
- No locale-key cleanup needed: the old quickNote.placeholder/send
keys lived only as t() fallbacks, not in en.json/ar.json.
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- HMR errors visible in the browser console were from the intermediate
syntax-error state during extraction; cleared after the final edit.
The yellow side tab on /executive-meetings previously opened a bespoke
textarea panel I added in #590. The user pointed out (screenshot) that
it should be the exact same composer card they use on /notes — yellow
background, color picker, label menu, checklist toggle, "حفظ" Save,
paper-plane Send.
Changes:
- Extracted Composer + its inline helpers (ChecklistEditor, KindToggle,
ColorPicker, LabelMenu, newItemId) out of pages/notes.tsx into a new
shared module at components/notes/composer.tsx.
- Extracted SendNoteDialog out of pages/notes.tsx into a new shared
module at components/notes/send-note-dialog.tsx.
- pages/notes.tsx now imports both modules; behavior of the notes page
is unchanged.
- pages/executive-meetings.tsx: deleted InlineQuickNotePanel. The
schedule view now renders <Composer folderSelection={{kind:"all"}}>
inside the yellow side-tab toggle, plus <SendNoteDialog> for the
paper-plane Send flow. A new quickNoteTrigger counter is bumped each
time the tab opens so the shared Composer auto-expands and focuses
its textarea. A small X in the corner dismisses the panel; Save just
collapses the composer (matches /notes UX), Send saves then opens
the recipient picker.
- Notes created from the meetings panel land as unfiled notes in the
user's notes (folder "all"), visible immediately in /notes.
Notes:
- No locale-key cleanup needed: the old quickNote.placeholder/send
keys lived only as t() fallbacks, not in en.json/ar.json.
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- HMR errors visible in the browser console were from the intermediate
syntax-error state during extraction; cleared after the final edit.
Bug: on iPad / iOS Safari, tapping a meetings-table editable cell hid
the floating formatting toolbar (color swatches, alignment, font, save,
cancel) under the soft keyboard. Users saw only Apple's tiny B/I/U
accessory bar and could not change color or even tap Save/Cancel.
Root cause in `artifacts/tx-os/src/components/editable-cell.tsx`:
FormattingToolbar positioned itself at `rect.bottom + 4` and explicitly
refused to flip above the cell. It also only listened to window scroll
and resize, not to `visualViewport` events, so it did not react to the
keyboard opening or closing.
Fix (single file):
- Import and call the existing `useVisualViewport` hook inside
FormattingToolbar to get reactive `{ height, offsetTop }`.
- Compute the visible viewport bottom as `offsetTop + height` and, when
the toolbar would not fit below the cell without going under that
bottom (keyboard or short viewport), flip it ABOVE the cell:
`top = max(vv.offsetTop + 4, rect.top - tbHeight - 4)`.
- Keep desktop behavior unchanged: when the viewport is full-height the
below position still fits, so we still place below the cell.
- Added `vv.height`, `vv.offsetTop`, `vv.keyboardInset` to the effect
deps so the toolbar tracks the keyboard show/hide animation.
- Replaced the "never flip above" comment block with a comment that
documents the keyboard-coverage exception.
Out of scope (unchanged): toolbar wrapping/overflow on very narrow
iPads, Apple's built-in B/I/U keyboard accessory bar, any other page.
Validation:
- `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
- Vite HMR updated editable-cell.tsx without errors.
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.
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.
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.
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).
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).
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).
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).
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.
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).
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.
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.