- /auth/logout: delete all push_subscriptions rows for the user before
destroying the session. Best-effort with logger.warn on failure so an
operator can correlate any leaked-ring report to a real DB error.
- home.tsx handleLogout: call pushSub.disable() before the logout
mutation, raced against a 1.5s timeout so a stalled service worker
cannot block sign-out.
- executive-meeting-scheduler: switch the eligibility filter from
denylist (ne cancelled + ne completed) to whitelist
(eq status='scheduled'). Postponed / rescheduled / future statuses
can no longer trigger false 5-minute reminders.
- docker-compose.yml: pin TZ=Asia/Riyadh on postgres, api, and web
services so the scheduler's naive date/time math matches the
operator's wall clock instead of UTC.
Gitea push failed with TLS error during this session — code committed
locally, needs manual `./scripts/publish-to-gitea.sh --push` retry
when the desktop-11cj93j tunnel recovers.
The four default home-screen tiles (Notes / Services / Meetings /
Admin) showed up as three blue tiles + one orange one. Root cause was
NOT the DB — seed.ts already gave each app a distinct hex — but the
`gradientForColor()` mapper in `artifacts/tx-os/src/pages/home.tsx`
only recognised blue/purple/green/orange/pink/teal, so yellow
(#eab308), red (#ef4444), and the dark navy meetings colour all fell
through to the default blue gradient.
Changes:
1. `artifacts/tx-os/src/index.css` — added `.icon-tile-red` and
`.icon-tile-yellow` gradients matching the existing tile style.
2. `artifacts/tx-os/src/pages/home.tsx` — `gradientForColor()` now
maps red/dc2626 and yellow/facc15 first. Purple branch stays for
admin-custom apps but no seeded row uses it.
3. `scripts/src/seed.ts` —
- Meetings: `#0B1E3F` → `#22c55e` (green).
- Notifications: `#8b5cf6` → `#06b6d4` (cyan) — kills the last
purple in the default palette per user direction.
4. `scripts/src/update-app-colors.ts` (new) — idempotent migration
that bumps existing rows ONLY when they still hold the exact old
default colour, so admin customisations from the Apps editor are
preserved. Wired into `scripts/package.json` as
`pnpm run update-app-colors`.
5. `scripts/redeploy.sh` — new step 5/6 runs the colour migration
after seed and before `docker compose up -d`, on the same one-shot
`migrate` container pattern as other one-shot scripts.
No schema changes, no new deps. tx-os tsc passes, scripts tsc passes.
After `./scripts/redeploy.sh` on Mac, home shows four distinct hues
(yellow / orange / green / red) and no tile is purple anywhere.
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
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
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).
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.
- 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.
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.
User report: in the Tx OS launcher, the dot above ج in "الاجتماعات"
(and similar diacritics on other tile labels) was being sliced off on
iPad — a visible visual-quality bug on the most-used screen.
Root cause (artifacts/tx-os/src/pages/home.tsx, AppIconContent):
the label span used `leading-tight` (line-height 1.25) + `truncate`
(overflow: hidden). At 11px the line-box was 13.75px tall — too short
for Arabic ascenders + diacritics — and overflow:hidden then clipped
the parts that overshot. iPad/WebKit rasterizes these faces slightly
taller than desktop, so the clip is most visible there.
Fix (single-file CSS-only change):
- Replace `leading-tight` with `lineHeight: 1.6` for plenty of room
above the baseline.
- Add `paddingTop: 2` as cheap insurance for any remaining overshoot.
- Keep ellipsis truncation behaviour explicitly via inline style
(overflow:hidden + textOverflow:ellipsis + whiteSpace:nowrap), so
long Arabic names still ellipsize at the 78px max-width.
- Add `block` so max-width applies cleanly.
No change to icon tile size, grid layout, badges, or any other
markup. Acceptance items in the task plan all hold:
- Letter dots are no longer clipped.
- Long names still truncate with an ellipsis at 78px.
- Desktop layout is visually unchanged.
Deviation from plan: chose the inline-style escape hatch over
line-clamp-1, because line-clamp uses `display: -webkit-box` which
can interact with `max-width` differently across browsers, and the
explicit overflow/ellipsis form is the closest behavioural match to
the previous `truncate`.
Problem: On iPad Safari, the executive-meetings app collapsed the URL
bar on scroll (native-app feel), but Notes and Home kept it visible
permanently because their root containers were not document-scrollable.
- notes.tsx used `h-screen ... overflow-hidden` with an internal
`flex-1 min-h-0 overflow-y-auto` scroll region.
- home.tsx used `min-h-screen ... overflow-hidden`.
With no document-level scroll, iOS Safari has no trigger to hide the
URL bar.
Fix (quick-fix path the user picked, no PWA install meta):
- notes.tsx: root → `min-h-[100dvh] os-bg flex flex-col` (removed
`overflow-hidden`, swapped `h-screen` → `min-h-[100dvh]`). Header now
`sticky top-0 z-10` so it stays pinned. Inner body wrapper changed
from `flex-1 min-h-0 overflow-y-auto flex flex-col` to
`flex-1 flex flex-col` so the document (not an inner div) scrolls —
giving Safari the trigger it needs.
- home.tsx: root → `min-h-[100dvh] os-bg flex flex-col relative`
(removed `overflow-hidden`).
Why `100dvh`: dynamic viewport height sizes against the *visible*
area, so the layout reflows cleanly when the URL bar appears/hides.
Out of scope (matches task): no PWA meta tags, no other pages touched
(spot-checked others: chat/services/orders/notifications/admin already
use min-h-screen without overflow-hidden), no styling changes, executive-
meetings page untouched (it's the reference).
Verification: tsc --noEmit clean. Pre-existing test workflow failure on
unrelated executive-meetings.ts type errors is out of scope.
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.
Two prior code-review rounds were addressed in this commit:
Round 1 (independence):
- Added immutable snapshot columns (title/content/color) on note_recipients.
- Dropped the FK from note_recipients.note_id and note_replies.note_id so
recipient threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot for
recipients (sender/admin still see the live note).
Round 2 (validation REJECT fixes):
- POST /notes/:id/reply: owner is now allowed to reply too. Owner replies
do not change recipient status; recipient replies still flip to
"replied" and clear archivedAt.
- Added GET /notes/:id as an alias of /notes/:id/thread (shared handler).
- Added OpenAPI ops for /notes/sent, /notes/received, /notes/{id},
/notes/{id}/send, /notes/{id}/read, /notes/{id}/archive,
/notes/{id}/reply, and ran orval codegen.
- use-notifications-socket.ts now shows bilingual toasts for note_received
and note_replied (suppressed during the socket warmup window).
- home.tsx renders an unread badge on the Notes app tile, fed by
useReceivedNotes(false) filtered to status === "unread"; refreshes
automatically on socket invalidation.
Tests: 7 backend tests in notes-share.test.mjs (independence,
archived-reply, owner-reply preserves recipient status, GET /notes/:id
alias, etc.) + the notes-inbox e2e all pass. tx-os typecheck is clean.
Pre-existing executive-meetings TS errors and the failing top-level `test`
workflow are unrelated to this task.
DB
- Added 7 user pref columns; vibration is now per-channel
(vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
sounds, vibration on, mute off, volume 70.
API
- PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial
update, integer guard for volume). buildAuthUser exposes all new fields.
- OpenAPI spec updated; orval client + zod regenerated.
Frontend
- Static sound library: 8 short WAV assets under public/sounds/ + manifest
(notification-sound-manifest.ts) mapping id -> labelKey -> URL.
- HTMLAudio-based player (notification-sounds.ts) with throttle, unlock,
cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture.
- Settings popover: global mute, volume, slot tabs (orders/meetings) with
per-channel sound + per-channel vibration + per-channel toggle, sound list
with one-tap previews. Concurrency-safe optimistic updates (monotonic seq
counter). RTL-correct toggle knob transforms.
- QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with
confirmation toast.
- useAudioUnlock: unlocks audio on first interaction.
- useAutoplayHint: toasts a localized "click anywhere to enable" hint when
the player reports a blocked play attempt.
- Socket hook plays the right per-channel sound + vibration on
notification_created (orders, executive_meeting) when the tab is hidden.
- UpcomingMeetingAlert: plays meeting reminder once per new eligible
meeting (deduped by meetingId, survives 30s polling refetches).
- AR/EN translations added.
Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
- DB: 7 new user prefs (sound per slot, per-type toggles, vibration, mute, volume).
- API: PATCH /auth/me/notification-preferences with whitelisted partial update,
integer guard for volume, and hydrated AuthUser response. AuthUser now exposes
the 7 new fields.
- Frontend: Web Audio synth library (8 sounds), settings popover with global
mute/vibration/volume/per-type toggles, slot tabs, sound preview buttons,
shift+click on bell for quick mute. Concurrency-safe optimistic updates with
monotonic seq counter. RTL-correct toggle knob transforms.
- Socket hook plays sound on notification_created (orders/meetings only) when
tab is hidden, respecting global mute and per-type toggles.
- Audio unlock hook mounted globally to satisfy autoplay restrictions.
- AR/EN translations added.
Pre-existing TS errors in executive-meetings.ts (font_settings) and pre-existing
test failures in test workflow are unrelated to this task.
Update `home.tsx` to make the top bar larger on medium screens and above, increasing icon sizes and padding for better usability on tablets and desktops.
## Task #80 — Tell receivers when an order is no longer available to claim
### Problem
When a receiver tried to act on an order that had already been cancelled, completed,
or claimed by someone else, the UI showed a generic "Could not complete the action"
toast. The stale card also stayed in the list, requiring a manual refresh.
### Changes
**artifacts/api-server/src/routes/service-orders.ts**
- PATCH /orders/:id/confirm-receipt: after a failed atomic claim, query the current
order status; return 409 `order_unavailable` if it's cancelled/completed (vs the
existing 409 `already_claimed` for when it was grabbed by someone else first)
- PATCH /orders/:id/status (preparing/completed branch): terminal-state check runs
BEFORE permission gating — returns 409 `order_unavailable` when order is already
cancelled/completed instead of ever hitting 403 Forbidden
- PATCH /orders/:id/status (cancel branch): same — terminal-state check moved to
the top of the branch so non-admin receivers trying to cancel an already-terminal
order get 409 `order_unavailable` (not 403 Forbidden which would never let the UI
remove the stale card)
**artifacts/tx-os/src/pages/orders-incoming.tsx**
- handleConfirmReceipt onError: distinguish 409 `order_unavailable` from
`already_claimed`, show specific toast, remove stale card via invalidate()
- handleSetStatus onError: same pattern — specific toast + invalidate() on
`order_unavailable`
- handleCancel onError: same pattern
**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `incomingOrders.orderUnavailable` ("This order is no longer available" /
"هذا الطلب لم يعد متاحاً")
## Bonus fix — Disabled app disappears from admin panel (user-reported)
### Problem
When an admin disabled an app via the toggle, the app disappeared from the admin
panel too (isActive filter applied to everyone), making it impossible to re-enable
without direct DB access.
### Changes
**artifacts/api-server/src/routes/apps.ts**
- getVisibleAppsForUser: admins now receive ALL apps including inactive ones via
conditional $dynamic() Drizzle query; non-admins still only see active apps
**artifacts/tx-os/src/pages/home.tsx**
- Filter orderedApps to only active apps before rendering so inactive apps don't
appear on the home screen even for admins
**artifacts/tx-os/src/pages/admin.tsx**
- Inactive app cards show reduced opacity + a "Disabled/معطّل" badge
**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `admin.appDisabled` ("Disabled" / "معطّل")