The amber "ملاحظة / Note" button (#600) lives in the Executive
Meetings page header. The whole header is hidden in fullscreen, so the
quick-note panel — which itself already works in fullscreen — had no
entry point. User had to exit fullscreen, add the note, then re-enter.
Added a floating amber pill next to the existing fullscreen-exit pill
at the top-end of the page. Mirrors the header button exactly:
- Same handler (setQuickNoteOpen + setQuickNoteTrigger).
- Same gating: only Schedule section, hidden while the panel is open
or while the bulk-actions toolbar is active.
- Same i18n key (executiveMeetings.quickNote.label) — no new strings.
- Same amber palette (bg-amber-100 / border-amber-300 / text-[#0B1E3F])
with the Plus icon and label.
- Positioned at `top-3 end-14` so it sits inboard of the exit pill
(`end-3`, w-9) in both LTR (to the left) and RTL (to the right),
with no overlap.
- `print:hidden`, `data-testid="em-fullscreen-quicknote"`.
Single JSX addition (~36 lines incl. comment) in
artifacts/tx-os/src/pages/executive-meetings.tsx, right after the
exit-fullscreen button. No other files touched. No new state, no new
i18n, no API changes, no dependency changes. Non-fullscreen layout is
untouched. tx-os tsc passes.
The amber "ملاحظة / Note" button (#600) lives in the Executive
Meetings page header. The whole header is hidden in fullscreen, so the
quick-note panel — which itself already works in fullscreen — had no
entry point. User had to exit fullscreen, add the note, then re-enter.
Added a floating amber pill next to the existing fullscreen-exit pill
at the top-end of the page. Mirrors the header button exactly:
- Same handler (setQuickNoteOpen + setQuickNoteTrigger).
- Same gating: only Schedule section, hidden while the panel is open
or while the bulk-actions toolbar is active.
- Same i18n key (executiveMeetings.quickNote.label) — no new strings.
- Same amber palette (bg-amber-100 / border-amber-300 / text-[#0B1E3F])
with the Plus icon and label.
- Positioned at `top-3 end-14` so it sits inboard of the exit pill
(`end-3`, w-9) in both LTR (to the left) and RTL (to the right),
with no overlap.
- `print:hidden`, `data-testid="em-fullscreen-quicknote"`.
Single JSX addition (~36 lines incl. comment) in
artifacts/tx-os/src/pages/executive-meetings.tsx, right after the
exit-fullscreen button. No other files touched. No new state, no new
i18n, no API changes, no dependency changes. Non-fullscreen layout is
untouched. tx-os tsc passes.
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.
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.
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).
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).
Update the label for the quick note button in the executive meetings header from "Take a note..." to "Note" in English and "اكتب ملاحظة…" to "ملاحظة" in Arabic.
User reported the yellow vertical "اكتب ملاحظة…" tab pinned to the page
edge (left-0 top-[40vh]) felt out of place and asked for it to live
inside the Executive Meetings header in a clear spot.
Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Added an amber-tinted Button in the header action cluster, placed
before the Export PDF button so it reads first in RTL. Uses
bg-amber-100 / border-amber-300 to keep the original yellow visual
identity while matching the other header buttons' shape and size.
Reuses the existing `executiveMeetings.quickNote.label` i18n key —
no new translations. testid: `em-quick-note-header`.
- Same gating as the old floating tab: section === "schedule" &&
!isFullscreen && !bulkToolbarActive && !quickNoteOpen, so the button
hides while the composer is open, in fullscreen, during bulk
selection, and on non-schedule sub-pages.
- Removed the `<MeetingsQuickNoteTab />` mount and the component
definition itself. Updated the stale comment on `bulkToolbarActive`
to refer to the header button.
Verified:
- `rg em-quick-note-tab|MeetingsQuickNoteTab` clean across repo (no
tests referenced the old testid).
- `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
No deviation from the plan.
User reported the yellow vertical "اكتب ملاحظة…" tab pinned to the page
edge (left-0 top-[40vh]) felt out of place and asked for it to live
inside the Executive Meetings header in a clear spot.
Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Added an amber-tinted Button in the header action cluster, placed
before the Export PDF button so it reads first in RTL. Uses
bg-amber-100 / border-amber-300 to keep the original yellow visual
identity while matching the other header buttons' shape and size.
Reuses the existing `executiveMeetings.quickNote.label` i18n key —
no new translations. testid: `em-quick-note-header`.
- Same gating as the old floating tab: section === "schedule" &&
!isFullscreen && !bulkToolbarActive && !quickNoteOpen, so the button
hides while the composer is open, in fullscreen, during bulk
selection, and on non-schedule sub-pages.
- Removed the `<MeetingsQuickNoteTab />` mount and the component
definition itself. Updated the stale comment on `bulkToolbarActive`
to refer to the header button.
Verified:
- `rg em-quick-note-tab|MeetingsQuickNoteTab` clean across repo (no
tests referenced the old testid).
- `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
No deviation from the plan.
In executive-meetings.tsx, the floating exit-fullscreen control was
a pill (`gap-1.5 px-3 py-1.5`) showing both a Minimize2 arrow and
the Arabic/English label "الخروج من ملء الشاشة" / "Exit fullscreen".
User asked for icon only.
Changes:
- Removed the `<span>{t(...)}</span>` label.
- Swapped pill classes for a circular icon button: `inline-flex
items-center justify-center rounded-full bg-[#0B1E3F] text-white
w-9 h-9 shadow-lg hover:bg-[#0B1E3F]/90 print:hidden`, keeping the
same `fixed top-3 end-3 z-50` positioning.
- Bumped the icon to `w-5 h-5` so it reads cleanly on its own.
- Added a `title` attribute (hover tooltip) mirroring the existing
`aria-label` — screen readers + mouse users still get the
localised "exit fullscreen" affordance.
- `data-testid="em-fullscreen-exit"` untouched, so existing tests
keep passing.
- No translation key changes; no change to the enter-fullscreen
toolbar button at L2958.
Verified `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
In executive-meetings.tsx, the floating exit-fullscreen control was
a pill (`gap-1.5 px-3 py-1.5`) showing both a Minimize2 arrow and
the Arabic/English label "الخروج من ملء الشاشة" / "Exit fullscreen".
User asked for icon only.
Changes:
- Removed the `<span>{t(...)}</span>` label.
- Swapped pill classes for a circular icon button: `inline-flex
items-center justify-center rounded-full bg-[#0B1E3F] text-white
w-9 h-9 shadow-lg hover:bg-[#0B1E3F]/90 print:hidden`, keeping the
same `fixed top-3 end-3 z-50` positioning.
- Bumped the icon to `w-5 h-5` so it reads cleanly on its own.
- Added a `title` attribute (hover tooltip) mirroring the existing
`aria-label` — screen readers + mouse users still get the
localised "exit fullscreen" affordance.
- `data-testid="em-fullscreen-exit"` untouched, so existing tests
keep passing.
- No translation key changes; no change to the enter-fullscreen
toolbar button at L2958.
Verified `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
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.
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.
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.
scripts/update-version.mjs now maintains a `deployCount` integer and
rewrites `version` as `${baseVersion}.${deployCount}` every time the
build hash actually changes. When build is unchanged (re-run without
new commit) it still no-ops, matching prior behaviour, so re-running
the build twice in CI doesn't inflate the counter.
version.json shape:
{
"baseVersion": "0.1.0-dev", <- stable, manually owned
"version": "0.1.0-dev.42", <- baseVersion + "." + deployCount
"build": "20260518.f942f101",
"deployCount": 42
}
Migration: on the very first run after this change, if `baseVersion`
is missing, the script derives it from the existing `version` field
by stripping any trailing `.<digits>`. If `deployCount` is missing it
seeds from that same trailing counter, so a repo that was already
hand-numbered (e.g. "0.1.0-dev.12") doesn't reset progress to 0.
Verified manually:
- Re-run with no new commit -> "build unchanged: ...", file untouched.
- New build -> bumps deployCount by exactly 1, rewrites version.
- Manually editing baseVersion to "0.2.0" -> next bump yields
"0.2.0.<count+1>", manual base preserved.
api-server reads version.json statically (import with type: "json"),
so no API change is needed — the new fields flow through to
/api/system/version automatically. admin.tsx just prints
data.current.version, no regex parsing, so the new suffixed string
shows up in the System Updates card with no UI edit.
version.json reset to baseVersion=0.1.0-dev, version=0.1.0-dev,
deployCount=0 so the next legitimate Mac docker build bumps cleanly
to dev.1 on first redeploy.
scripts/update-version.mjs now maintains a `deployCount` integer and
rewrites `version` as `${baseVersion}.${deployCount}` every time the
build hash actually changes. When build is unchanged (re-run without
new commit) it still no-ops, matching prior behaviour, so re-running
the build twice in CI doesn't inflate the counter.
version.json shape:
{
"baseVersion": "0.1.0-dev", <- stable, manually owned
"version": "0.1.0-dev.42", <- baseVersion + "." + deployCount
"build": "20260518.f942f101",
"deployCount": 42
}
Migration: on the very first run after this change, if `baseVersion`
is missing, the script derives it from the existing `version` field
by stripping any trailing `.<digits>`. If `deployCount` is missing it
seeds from that same trailing counter, so a repo that was already
hand-numbered (e.g. "0.1.0-dev.12") doesn't reset progress to 0.
Verified manually:
- Re-run with no new commit -> "build unchanged: ...", file untouched.
- New build -> bumps deployCount by exactly 1, rewrites version.
- Manually editing baseVersion to "0.2.0" -> next bump yields
"0.2.0.<count+1>", manual base preserved.
api-server reads version.json statically (import with type: "json"),
so no API change is needed — the new fields flow through to
/api/system/version automatically. admin.tsx just prints
data.current.version, no regex parsing, so the new suffixed string
shows up in the System Updates card with no UI edit.
version.json reset to baseVersion=0.1.0-dev, version=0.1.0-dev,
deployCount=0 so the next legitimate Mac docker build bumps cleanly
to dev.1 on first redeploy.
Two changes to artifacts/tx-os/src/pages/executive-meetings.tsx,
both gated on `isPresidentView` so non-president users are unaffected.
1. Schedule search hidden for the President
- Wrapped <SearchToggle> in ScheduleSection (L2945) in
{!isPresidentView && (...)}.
- Kept searchQuery state + meetingMatchesSearch wiring intact:
when query is "" the filter is a no-op, so the President sees
the full day's meetings, and the box reappears for anyone else.
2. Gear button so the President can reach Settings (font controls)
- Added an icon-only gear button (SettingsIcon, lucide-react —
already imported) in the header right-side block (L1113),
visible only when isPresidentView && section === "schedule".
data-testid="em-president-settings".
- Added an icon+label "back to schedule" button (ArrowRight for
RTL / ArrowLeft for LTR) shown when isPresidentView &&
section === "settings". data-testid="em-president-back-to-schedule".
- Relaxed the force-schedule effect (L966) so the President is
only redirected when section is neither "schedule" nor
"settings" — otherwise the gear tap would bounce back instantly.
- Sub-nav stays hidden (`!isPresidentView` on the SECTIONS map
is unchanged). Gear is the only entry point.
- No backend, no new i18n keys (reuses
executiveMeetings.nav.settings / .schedule).
- FontSettingsSection already renders for canRead users, and
canEditGlobalFontSettings stays as the API reports it — no
extra client-side gate.
`pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
Two changes to artifacts/tx-os/src/pages/executive-meetings.tsx,
both gated on `isPresidentView` so non-president users are unaffected.
1. Schedule search hidden for the President
- Wrapped <SearchToggle> in ScheduleSection (L2945) in
{!isPresidentView && (...)}.
- Kept searchQuery state + meetingMatchesSearch wiring intact:
when query is "" the filter is a no-op, so the President sees
the full day's meetings, and the box reappears for anyone else.
2. Gear button so the President can reach Settings (font controls)
- Added an icon-only gear button (SettingsIcon, lucide-react —
already imported) in the header right-side block (L1113),
visible only when isPresidentView && section === "schedule".
data-testid="em-president-settings".
- Added an icon+label "back to schedule" button (ArrowRight for
RTL / ArrowLeft for LTR) shown when isPresidentView &&
section === "settings". data-testid="em-president-back-to-schedule".
- Relaxed the force-schedule effect (L966) so the President is
only redirected when section is neither "schedule" nor
"settings" — otherwise the gear tap would bounce back instantly.
- Sub-nav stays hidden (`!isPresidentView` on the SECTIONS map
is unchanged). Gear is the only entry point.
- No backend, no new i18n keys (reuses
executiveMeetings.nav.settings / .schedule).
- FontSettingsSection already renders for canRead users, and
canEditGlobalFontSettings stays as the API reports it — no
extra client-side gate.
`pnpm --filter @workspace/tx-os exec tsc --noEmit` clean.
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 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.
Hide the absolutely positioned HeaderClock on mobile viewports to prevent it from overlapping with the header title and action buttons, restoring it on larger screens.
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.
- 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.