Three small UX fixes on /executive-meetings (schedule tab):
1) HeaderClock: dropped the digital "HH:MM:SS م" string next to the
weekday — the analog dial already conveys the live time, so it was
redundant. Bumped AnalogClock size from 36 to 44 so the dial reads
cleanly without the digital crutch. Weekday + date stay (calendar
info, not clock info). Removed the now-unused `useState`/`useEffect`
ticker inside HeaderClock — AnalogClock has its own.
2) "اكتب ملاحظة" yellow side tab no longer navigates away to /notes.
Added a transient `quickNoteOpen` local state in
ExecutiveMeetingsPageInner. The tab's onClick now toggles it on; a
new `InlineQuickNotePanel` is rendered above <ScheduleSection> with
an auto-focused textarea, Cancel/Send buttons, Cmd/Ctrl+Enter to
submit, Esc to close. Submit POSTs to /api/notes with the existing
contract ({ content, color: "yellow" }) and closes on success. The
side tab hides while the panel is open so they don't double up.
State auto-resets when the user leaves the schedule tab.
3) Fullscreen exit pill overlap: the floating "الخروج من ملء الشاشة"
pill (fixed top-3 end-3) was covering the schedule table's "الوقت"
column header. Added `pt-12 sm:pt-14` to the fullscreen branch of
<main> so the table starts below the pill on iPad landscape
(1024-wide). Non-fullscreen layout unchanged.
i18n: added executiveMeetings.quickNote.{placeholder,send} in EN+AR;
existing executiveMeetings.quickNote.label reused for the panel
header. All keys also have inline fallbacks so missing translations
don't break.
Drive-by: fixed a TypeScript narrowing error in admin.tsx left over
from the prior task's review-comment cleanup (data.startedAt is
already typed as string, so the typeof check produced `never` in the
else branch — replaced with a direct call).
Verified: pnpm --filter @workspace/tx-os exec tsc --noEmit passes
clean, Vite HMR updated cleanly in dev.
Three small UX fixes on /executive-meetings (schedule tab):
1) HeaderClock: dropped the digital "HH:MM:SS م" string next to the
weekday — the analog dial already conveys the live time, so it was
redundant. Bumped AnalogClock size from 36 to 44 so the dial reads
cleanly without the digital crutch. Weekday + date stay (calendar
info, not clock info). Removed the now-unused `useState`/`useEffect`
ticker inside HeaderClock — AnalogClock has its own.
2) "اكتب ملاحظة" yellow side tab no longer navigates away to /notes.
Added a transient `quickNoteOpen` local state in
ExecutiveMeetingsPageInner. The tab's onClick now toggles it on; a
new `InlineQuickNotePanel` is rendered above <ScheduleSection> with
an auto-focused textarea, Cancel/Send buttons, Cmd/Ctrl+Enter to
submit, Esc to close. Submit POSTs to /api/notes with the existing
contract ({ content, color: "yellow" }) and closes on success. The
side tab hides while the panel is open so they don't double up.
State auto-resets when the user leaves the schedule tab.
3) Fullscreen exit pill overlap: the floating "الخروج من ملء الشاشة"
pill (fixed top-3 end-3) was covering the schedule table's "الوقت"
column header. Added `pt-12 sm:pt-14` to the fullscreen branch of
<main> so the table starts below the pill on iPad landscape
(1024-wide). Non-fullscreen layout unchanged.
i18n: added executiveMeetings.quickNote.{placeholder,send} in EN+AR;
existing executiveMeetings.quickNote.label reused for the panel
header. All keys also have inline fallbacks so missing translations
don't break.
Drive-by: fixed a TypeScript narrowing error in admin.tsx left over
from the prior task's review-comment cleanup (data.startedAt is
already typed as string, so the typeof check produced `never` in the
else branch — replaced with a direct call).
Verified: pnpm --filter @workspace/tx-os exec tsc --noEmit passes
clean, Vite HMR updated cleanly in dev.
Problem: After `git pull && docker compose up -d --build api` on the
Mac, the admin → System Updates panel kept showing the same version
(0.1.0-dev) and the same build (20260517.8a5bd5db) because both come
from version.json, a hand-edited file that nothing rewrites on every
build. No visible signal that a new image was actually running.
Two independent signals were added — neither needs editing version.json
by hand:
1) Auto-stamp version.json at Docker build time
- docker/api-server.Dockerfile: new ARGs GIT_SHA and BUILD_DATE.
A `node -e` step rewrites version.json's `build` field to
`${YYYYMMDDTHHmm}.${sha}` (e.g. 20260518T1042.71da8e01). When
the args are missing/"unknown", version.json is left untouched
so plain `docker build` and Replit `pnpm dev` still work.
- docker-compose.yml: api.build.args wires through GIT_SHA and
BUILD_DATE with `${GIT_SHA:-unknown}` fallbacks.
- scripts/redeploy.sh: exports GIT_SHA (git rev-parse --short HEAD)
and BUILD_DATE (date -u +%Y-%m-%dT%H:%M:%SZ) before `docker
compose build`, so the helper script just works.
2) startedAt timestamp from the API
- artifacts/api-server/src/routes/system.ts: SERVER_STARTED_AT
captured at module load (frozen for the process lifetime) and
added to every branch of GET /system/version (not-configured,
error, invalid-version, up-to-date, update-available, catch).
- lib/api-spec/openapi.yaml: added `startedAt: date-time` to
SystemVersionResponse (required). Regenerated api-client-react
and api-zod via `pnpm --filter @workspace/api-spec run codegen`.
3) Admin UI surfacing both signals
- artifacts/tx-os/src/pages/admin.tsx: new formatBuildStamp parses
`YYYYMMDD[THHmm].sha` and renders a localized date next to the
raw token. A new "Server started" line below the build number
formats the boot timestamp via the existing formatChecked helper.
- artifacts/tx-os/src/locales/{en,ar}.json: added
admin.systemUpdates.serverStartedAt ("Server started" /
"بدأ تشغيل الخادم").
4) Docs
- replit.md: documented how to verify a real redeploy via the
System Updates panel and the manual one-liner for `docker
compose build` without the helper script.
Verified: codegen succeeds, API workflow restarted clean, Vite served
fine. Pre-existing typecheck errors in push.ts and font-settings are
unrelated and untouched.
Mac next steps:
cd ~/Downloads/TX && git pull && ./scripts/redeploy.sh
Then open admin → System Updates: build line should show a new
`YYYYMMDDTHHmm.sha` stamp and "Server started" should reflect the
new boot time.
Problem: After `git pull && docker compose up -d --build api` on the
Mac, the admin → System Updates panel kept showing the same version
(0.1.0-dev) and the same build (20260517.b5efd9eb) because both come
from version.json, a hand-edited file that nothing rewrites on every
build. No visible signal that a new image was actually running.
Two independent signals were added — neither needs editing version.json
by hand:
1) Auto-stamp version.json at Docker build time
- docker/api-server.Dockerfile: new ARGs GIT_SHA and BUILD_DATE.
A `node -e` step rewrites version.json's `build` field to
`${YYYYMMDDTHHmm}.${sha}` (e.g. 20260518T1042.a34eb5f5). When
the args are missing/"unknown", version.json is left untouched
so plain `docker build` and Replit `pnpm dev` still work.
- docker-compose.yml: api.build.args wires through GIT_SHA and
BUILD_DATE with `${GIT_SHA:-unknown}` fallbacks.
- scripts/redeploy.sh: exports GIT_SHA (git rev-parse --short HEAD)
and BUILD_DATE (date -u +%Y-%m-%dT%H:%M:%SZ) before `docker
compose build`, so the helper script just works.
2) startedAt timestamp from the API
- artifacts/api-server/src/routes/system.ts: SERVER_STARTED_AT
captured at module load (frozen for the process lifetime) and
added to every branch of GET /system/version (not-configured,
error, invalid-version, up-to-date, update-available, catch).
- lib/api-spec/openapi.yaml: added `startedAt: date-time` to
SystemVersionResponse (required). Regenerated api-client-react
and api-zod via `pnpm --filter @workspace/api-spec run codegen`.
3) Admin UI surfacing both signals
- artifacts/tx-os/src/pages/admin.tsx: new formatBuildStamp parses
`YYYYMMDD[THHmm].sha` and renders a localized date next to the
raw token. A new "Server started" line below the build number
formats the boot timestamp via the existing formatChecked helper.
- artifacts/tx-os/src/locales/{en,ar}.json: added
admin.systemUpdates.serverStartedAt ("Server started" /
"بدأ تشغيل الخادم").
4) Docs
- replit.md: documented how to verify a real redeploy via the
System Updates panel and the manual one-liner for `docker
compose build` without the helper script.
Verified: codegen succeeds, API workflow restarted clean, Vite served
fine. Pre-existing typecheck errors in push.ts and font-settings are
unrelated and untouched.
Mac next steps:
cd ~/Downloads/TX && git pull && ./scripts/redeploy.sh
Then open admin → System Updates: build line should show a new
`YYYYMMDDTHHmm.sha` stamp and "Server started" should reflect the
new boot time.
The HTML/puppeteer PDF renderer in
artifacts/api-server/src/lib/pdf-html-renderer.ts mapped every branded
sans family to NotoSansArabic-*.ttf — so even though the user picked
"DIN Next LT Arabic" in settings, the embedded glyph shapes were Noto
Sans Arabic. Same silent substitution applied to Tajawal, Helvetica
Neue LT Arabic, and Majalla. (pdf-renderer.ts was already wired
correctly to the real files in an earlier change; pdf-html-renderer.ts
was missed.)
Fix in pdf-html-renderer.ts FONT_FILES:
DIN Next LT Arabic → DINNextLTArabic-Regular/Bold.ttf
Tajawal → Tajawal-Regular/Bold.ttf
Helvetica Neue LT Arabic → HelveticaNeueLTArabic-Regular/Bold.ttf
Majalla → Majalla-Regular/Bold.ttf
system → NotoNaskhArabic-Regular/Bold.ttf (unchanged)
Helvetica Neue → HelveticaNeue-Regular/Bold.ttf (unchanged)
All target .ttf files were already bundled in
artifacts/api-server/assets/fonts/ — no asset changes needed.
Updated the matching test assertions in
artifacts/api-server/tests/executive-meetings.test.mjs so the
DIN Next LT Arabic path now expects /DINNextLTArabic/ in the PDF
bytes, and the Majalla path expects /Majalla/ instead of
/NotoNaskhArabic/. Kept the negative assertion that Majalla never
embeds a sans face.
API workflow restarted clean. Mac rebuild:
cd ~/Downloads/TX && git pull && docker compose up -d --build api
The HTML/puppeteer PDF renderer in
artifacts/api-server/src/lib/pdf-html-renderer.ts mapped every branded
sans family to NotoSansArabic-*.ttf — so even though the user picked
"DIN Next LT Arabic" in settings, the embedded glyph shapes were Noto
Sans Arabic. Same silent substitution applied to Tajawal, Helvetica
Neue LT Arabic, and Majalla. (pdf-renderer.ts was already wired
correctly to the real files in an earlier change; pdf-html-renderer.ts
was missed.)
Fix in pdf-html-renderer.ts FONT_FILES:
DIN Next LT Arabic → DINNextLTArabic-Regular/Bold.ttf
Tajawal → Tajawal-Regular/Bold.ttf
Helvetica Neue LT Arabic → HelveticaNeueLTArabic-Regular/Bold.ttf
Majalla → Majalla-Regular/Bold.ttf
system → NotoNaskhArabic-Regular/Bold.ttf (unchanged)
Helvetica Neue → HelveticaNeue-Regular/Bold.ttf (unchanged)
All target .ttf files were already bundled in
artifacts/api-server/assets/fonts/ — no asset changes needed.
Updated the matching test assertions in
artifacts/api-server/tests/executive-meetings.test.mjs so the
DIN Next LT Arabic path now expects /DINNextLTArabic/ in the PDF
bytes, and the Majalla path expects /Majalla/ instead of
/NotoNaskhArabic/. Kept the negative assertion that Majalla never
embeds a sans face.
API workflow restarted clean. Mac rebuild:
cd ~/Downloads/TX && git pull && docker compose up -d --build api
The HTML/puppeteer PDF renderer in
artifacts/api-server/src/lib/pdf-html-renderer.ts mapped every branded
sans family to NotoSansArabic-*.ttf — so even though the user picked
"DIN Next LT Arabic" in settings, the embedded glyph shapes were Noto
Sans Arabic. Same silent substitution applied to Tajawal, Helvetica
Neue LT Arabic, and Majalla. (pdf-renderer.ts was already wired
correctly to the real files in an earlier change; pdf-html-renderer.ts
was missed.)
Fix in pdf-html-renderer.ts FONT_FILES:
DIN Next LT Arabic → DINNextLTArabic-Regular/Bold.ttf
Tajawal → Tajawal-Regular/Bold.ttf
Helvetica Neue LT Arabic → HelveticaNeueLTArabic-Regular/Bold.ttf
Majalla → Majalla-Regular/Bold.ttf
system → NotoNaskhArabic-Regular/Bold.ttf (unchanged)
Helvetica Neue → HelveticaNeue-Regular/Bold.ttf (unchanged)
All target .ttf files were already bundled in
artifacts/api-server/assets/fonts/ — no asset changes needed.
Updated the matching test assertions in
artifacts/api-server/tests/executive-meetings.test.mjs so the
DIN Next LT Arabic path now expects /DINNextLTArabic/ in the PDF
bytes, and the Majalla path expects /Majalla/ instead of
/NotoNaskhArabic/. Kept the negative assertion that Majalla never
embeds a sans face.
API workflow restarted clean. Mac rebuild:
cd ~/Downloads/TX && git pull && docker compose up -d --build api
The HTML/puppeteer PDF renderer in
artifacts/api-server/src/lib/pdf-html-renderer.ts mapped every branded
sans family to NotoSansArabic-*.ttf — so even though the user picked
"DIN Next LT Arabic" in settings, the embedded glyph shapes were Noto
Sans Arabic. Same silent substitution applied to Tajawal, Helvetica
Neue LT Arabic, and Majalla. (pdf-renderer.ts was already wired
correctly to the real files in an earlier change; pdf-html-renderer.ts
was missed.)
Fix in pdf-html-renderer.ts FONT_FILES:
DIN Next LT Arabic → DINNextLTArabic-Regular/Bold.ttf
Tajawal → Tajawal-Regular/Bold.ttf
Helvetica Neue LT Arabic → HelveticaNeueLTArabic-Regular/Bold.ttf
Majalla → Majalla-Regular/Bold.ttf
system → NotoNaskhArabic-Regular/Bold.ttf (unchanged)
Helvetica Neue → HelveticaNeue-Regular/Bold.ttf (unchanged)
All target .ttf files were already bundled in
artifacts/api-server/assets/fonts/ — no asset changes needed.
Updated the matching test assertions in
artifacts/api-server/tests/executive-meetings.test.mjs so the
DIN Next LT Arabic path now expects /DINNextLTArabic/ in the PDF
bytes, and the Majalla path expects /Majalla/ instead of
/NotoNaskhArabic/. Kept the negative assertion that Majalla never
embeds a sans face.
API workflow restarted clean. Mac rebuild:
cd ~/Downloads/TX && git pull && docker compose up -d --build api
The api-server docker build failed on the Mac with:
ERROR: Could not resolve "../../../../version.json"
src/routes/system.ts:4
`version.json` lives at the repo root and is tracked in git, but
`docker/api-server.Dockerfile` never copied it into the `build` stage,
so esbuild (running in /app/artifacts/api-server) couldn't resolve the
`../../../../version.json` import added by commit cb894f7b.
Fix: added `COPY version.json ./` in the build stage, right after the
tsconfig copy and before `lib`/`scripts`/`artifacts` are copied. No
runtime change is needed — esbuild inlines the JSON into the bundle
via the `with { type: "json" }` import attribute, so the runtime stage
does not need the file on disk.
Local dev (`pnpm --filter @workspace/api-server run dev`) is
unaffected because it builds outside Docker against the real repo
root. tx-os Dockerfile not touched.
Mac side after pulling this commit:
cd ~/Downloads/TX && git pull && docker compose up -d --build api
The api-server docker build failed on the Mac with:
ERROR: Could not resolve "../../../../version.json"
src/routes/system.ts:4
`version.json` lives at the repo root and is tracked in git, but
`docker/api-server.Dockerfile` never copied it into the `build` stage,
so esbuild (running in /app/artifacts/api-server) couldn't resolve the
`../../../../version.json` import added by commit 80429920.
Fix: added `COPY version.json ./` in the build stage, right after the
tsconfig copy and before `lib`/`scripts`/`artifacts` are copied. No
runtime change is needed — esbuild inlines the JSON into the bundle
via the `with { type: "json" }` import attribute, so the runtime stage
does not need the file on disk.
Local dev (`pnpm --filter @workspace/api-server run dev`) is
unaffected because it builds outside Docker against the real repo
root. tx-os Dockerfile not touched.
Mac side after pulling this commit:
cd ~/Downloads/TX && git pull && docker compose up -d --build api
Two small fixes in artifacts/api-server/src/lib/pdf-html-renderer.ts:
1. Removed `font-weight: bold` from `.num-cell` so the # column inherits
the body font weight (same as meeting/attendees/time columns). The
header (`thead th`) keeps its bold styling unchanged.
2. Changed attendees and time cells in `buildMeetingRow` to use
`coloredStyle` (was empty string ""), so when a row has a `rowColor`,
the tint covers the whole row instead of only the # and meeting
cells. The `numDarkStyle` (dark bg + white digit for variants with
a border color) is preserved.
On-screen schedule, merged-cell rows, and cancelled-row red border
behavior are unchanged. API server restarted clean.
Two small fixes in artifacts/api-server/src/lib/pdf-html-renderer.ts:
1. Removed `font-weight: bold` from `.num-cell` so the # column inherits
the body font weight (same as meeting/attendees/time columns). The
header (`thead th`) keeps its bold styling unchanged.
2. Changed attendees and time cells in `buildMeetingRow` to use
`coloredStyle` (was empty string ""), so when a row has a `rowColor`,
the tint covers the whole row instead of only the # and meeting
cells. The `numDarkStyle` (dark bg + white digit for variants with
a border color) is preserved.
On-screen schedule, merged-cell rows, and cancelled-row red border
behavior are unchanged. API server restarted clean.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
- Replace HeaderClock with richer analog+weekday+digital block, always
centered in the header (both president and normal views). Drop the
PresidentClockBar render from the toolbar; the dead helper is left
in place but unused.
- Lift fullscreen state to ExecutiveMeetingsPageInner with
sessionStorage persistence, Esc handler, auto-exit when the user
leaves the schedule tab. Hide the page header when active and show a
floating "Exit fullscreen" pill. New Maximize/Minimize toggle in the
schedule toolbar portal.
- Add MeetingsQuickNoteTab: a fixed amber vertical pill pinned to the
left edge of the schedule view that navigates to /notes?new=1.
- notes.tsx: detect ?new=1, switch to the Active tab, bump a numeric
openTrigger that the top Composer reacts to by opening + focusing
the textarea, then scrub the query param so navigation doesn't
re-trigger.
- i18n: add executiveMeetings.fullscreen.{enter,exit} and
executiveMeetings.quickNote.label in ar.json and en.json.
tsc clean. Push to Gitea still blocked by Tailscale; user to retry.
Update toolbar positioning logic to flip above the editor when necessary to prevent it from being hidden by the visual viewport or the software keyboard, addressing issue #581.
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.
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.
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.
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.