1659dc4b684911689d679995ca23bea499e52b1e
714 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1659dc4b68 |
Task #589: Live build stamp + server start time in System Updates panel
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.
|
||
|
|
a34eb5f56d |
Task #588: PDF uses real DIN Next LT Arabic (and other branded fonts)
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 |
||
|
|
0689f5986c |
Task #588: PDF uses real DIN Next LT Arabic (and other branded fonts)
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 |
||
|
|
f26b6da4af |
Task #586: Unify PDF column font & row shading
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. |
||
|
|
15bec59724 |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- 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.
|
||
|
|
a487c1661d |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- 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.
|
||
|
|
fda6a38128 |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- 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.
|
||
|
|
224bf49783 |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- 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.
|
||
|
|
e296216c71 |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- 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.
|
||
|
|
f39d8edea2 |
feat(meetings): center clock, fullscreen toggle, quick-note tab (#583)
- 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.
|
||
|
|
207bbb482c |
Improve toolbar positioning to avoid keyboard obstruction
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. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fb6e8c2d-05ca-4eef-bd9b-dcf819bfcfcd Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true |
||
|
|
7bb27527fb |
editable-cell: keep formatting toolbar above the iPad keyboard (#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.
|
||
|
|
3d3a4ba972 |
notes: file-manager layout, folders section header, kill grid gap (#578)
Cleans up the Notes page after a user creates their first folder: 1. FoldersRail: in `rail` mode (every view except "All notes"), insert a small "My folders" / "مجلداتي" section header above the user's folder list so it no longer visually nests under the dark "Unfiled" pill. New i18n key `notes.folders.myFolders` in en.json and ar.json. 2. notes.tsx filteredNotes: when `folderSelection.kind === "all"`, hide notes whose `folderId !== null`. Combined with the existing FoldersBrowser cards this gives a file-manager feel — notes inside a folder are reachable by opening the folder card and no longer appear in the flat list, so users stop thinking move = copy. Counts on the "All notes" rail entry stay as the total (totalCount prop unchanged). 3. notes.tsx grid: replaced the two separate grid cells for composer/content with a single wrapper that uses `display: contents` on mobile (children flow into the outer grid honoring order-1/2/3) and `flex flex-col gap-4` on md+ (composer and content stack in the same cell, no empty grid-row gap when the FoldersRail is taller than the composer). Removed unused conditional row-start/row-span on the inner content div. Out of scope per plan: dnd-kit behavior, shared-with-me logic, composer or folder card redesign. Validation: `pnpm --filter @workspace/tx-os exec tsc --noEmit` clean. Vite HMR successful after fix; no babel/JSX errors. |
||
|
|
d547d695e4 |
#577: clean up reschedule dialog layout on iPad landscape
The Postpone dialog's "إعادة الجدولة" tab was packing Date + Start + End into a single 3-column row at `lg:` (≥1024 px), which on iPad landscape (and small desktops) made iOS Safari's native date/time spinner glyphs + Arabic AM/PM markers visibly collide between cells. User reported the boxes looked "غير مرتبه". Change: - artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx: replace the `grid grid-cols-1 gap-3 lg:grid-cols-3` wrapper with a vertical `space-y-3` stack: Date input on its own full-width row, then a `grid grid-cols-2 gap-3` row holding Start + End side-by- side. Same `min-w-0` + `w-full` discipline on every cell so inputs respect their track. - Updated the explanatory comment to record why we no longer try a 3-column path: the only layout that stays clean from 320 px phones up through desktop without depending on a custom picker is the Date-on-top / Start+End-below stack. Verified `tsc --noEmit` clean. No DB / locale / API changes. Mac rebuild = `git pull && ./start.sh rebuild && docker compose up -d --force-recreate web`. No script run needed. |
||
|
|
aba984d238 |
Improve handling of notification permission prompts
Update notification permission logic to differentiate between denied and dismissed states, and adjust locale messages accordingly. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 02bc349c-c9f1-47f4-967c-cfefeabde8bb Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true |
||
|
|
558d395e0c |
#576: diagnose push enable + open full edit page from quick actions
Three connected fixes on the Executive Meetings schedule.
1) Push enable diagnostics (use-push-subscription.ts):
- `enable()` now returns { ok, reason } instead of a bare boolean.
- Reasons: unsupported, unsupported_ios_safari, permission_denied,
vapid_unavailable, subscribe_failed, server_rejected.
- Detects iPhone/iPad Safari not running as a PWA (checks
navigator.standalone + display-mode media query, with iPadOS
MacIntel + maxTouchPoints fallback) and short-circuits with
unsupported_ios_safari so the user gets a concrete
"open from Home Screen first" message instead of a silent fail.
- Wrapped VAPID fetch and pushManager.subscribe in their own
try/catches so each failure mode maps to its own reason.
2) Specific failure toasts (push-enable-prompt.tsx,
notification-settings.tsx):
- Both call sites now look up
`notifSettings.push.errors.<reason>` and fall back to the
legacy generic copy if the key is missing.
- Added enableSuccess toast on the toggle path too — previously
only the prompt-card path showed feedback on success.
- New AR + EN strings under notifSettings.push.errors.*.
3) Quick-actions row sheet (executive-meetings.tsx):
- Edit + Postpone buttons now sit SIDE BY SIDE on every viewport
(flex + flex-1 + min-w-0 instead of flex-wrap + min-w-[11rem]),
same height, equal width — fixes the stacked / over-sized Edit
button shown in the user's iPhone screenshot.
- "Edit" no longer opens the small Meeting edit Dialog. It now
flips the whole schedule into edit mode (same as the toolbar
pencil) and paints a transient accent ring on the originating
row (~1.6s) + smooth-scrolls it into view. Matches image 3 in
#576 where the user expects to see the full editable list.
- Added `highlightEdit` prop on MeetingRow that composes an
extra inset+outer ring on top of the existing quickOpen /
selection / current / press shadows, plus a soft background
tint, and clears automatically when ScheduleSection clears
highlightEditRowId.
Out of scope (left for follow-up tasks): server-side push delivery
(payloads/sounds), the edit-mode UI itself.
|
||
|
|
039e251c6a |
#575: IT-P brand icon + login hero image
Replace generic PWA/favicon assets with the IT-P brand image and layer
the same image over the login page's AnimatedArt panel.
- Generated icon-192/512, maskable-512, and apple-touch-icon (180) from
the attached IT-P photo via ImageMagick (center crop 1110x1110, then
resize). Maskable has #0B1E3F padding so the safe zone is respected
on Android adaptive icons.
- Copied source as public/brand-itp.jpg for the login hero.
- index.html: replaced the generic favicon.svg link with a full
favicon set built from IT-P (favicon.ico + 16/32/48/192/512 PNG),
all cache-busted with ?v=itp1. Deleted public/favicon.svg so no
browser can fall back to the old generic mark.
- Cache-busted apple-touch-icon link the same way so iPad PWA
re-installs pick up the new artwork.
- manifest.webmanifest: same ?v=itp1 cache-bust on all icon entries.
- login.tsx: layered IT-P image over AnimatedArt with mix-blend-screen
(opacity 90%) plus top/bottom and right-edge dark gradients so the
composition reads as one piece and the right edge fades cleanly into
the form panel. Still hidden on < lg per scope.
Did not change favicon.svg itself (kept as last-resort vector
fallback). Mobile (< lg) shows no hero — explicit choice per plan
("إخفاؤها بشكل مدروس").
|
||
|
|
192f412f2a |
#575: IT-P brand icon + login hero image
Replace generic PWA/favicon assets with the IT-P brand image and layer
the same image over the login page's AnimatedArt panel.
- Generated icon-192/512, maskable-512, and apple-touch-icon (180) from
the attached IT-P photo via ImageMagick (center crop 1110x1110, then
resize). Maskable has #0B1E3F padding so the safe zone is respected
on Android adaptive icons.
- Copied source as public/brand-itp.jpg for the login hero.
- index.html: added explicit PNG icon links (192, 512) before the
existing favicon.svg fallback, plus cache-bust query (?v=itp1) on
apple-touch-icon so iPad PWA re-installs pick up the new artwork.
- manifest.webmanifest: same ?v=itp1 cache-bust on all icon entries.
- login.tsx: layered IT-P image over AnimatedArt with mix-blend-screen
(opacity 90%) plus top/bottom and right-edge dark gradients so the
composition reads as one piece and the right edge fades cleanly into
the form panel. Still hidden on < lg per scope.
Did not change favicon.svg itself (kept as last-resort vector
fallback). Mobile (< lg) shows no hero — explicit choice per plan
("إخفاؤها بشكل مدروس").
|
||
|
|
2f591a6ba8 |
Show the notifications app on the main page again
Remove hardcoded exclusion for the notifications app in the home page component. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 58af0916-ab79-457c-868e-3b72336f6067 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true |
||
|
|
b5efd9eb12 |
Add system updates page to admin section
Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1e6de894-7c78-4557-b01a-a2c1c01f4155 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true |
||
|
|
191e072353 |
#571: ship Calendar/Notifications disabled by default + drop Notifications tile from home
Task: on fresh installs the Calendar and Notifications apps were appearing enabled by default, and the home screen showed a Notifications tile that was redundant with the bell icon already in the top bar. User wanted both apps disabled by default and the home tile gone (bell stays). Changes: - scripts/src/seed.ts: flip the seed `isActive` for `notifications` and `calendar` from true → false. The seed inserts apps with `db.insert(appsTable).values(apps).onConflictDoNothing()`, so this is fully idempotent: existing environments (where the row already exists with isActive=true) are NOT changed; only fresh inserts on new installs pick up the disabled default. Admins can enable either app from app settings when they want it. - artifacts/tx-os/src/pages/home.tsx: remove "notifications" from the `dockApps` filter so the bottom dock no longer renders a Notifications tile. Filter becomes `["services", "admin"]`. The top-bar bell icon (and its unread badge) was untouched and still routes to `/notifications`. Out of scope (per spec): no migration to disable existing prod rows; no role/permission changes; bell icon in the top bar stays. Files: - scripts/src/seed.ts (notifications block ~L229, calendar block ~L275) - artifacts/tx-os/src/pages/home.tsx:532 |
||
|
|
2aa4a12ddc |
#571: ship Calendar/Notifications disabled by default + drop Notifications tile from home
Task: on fresh installs the Calendar and Notifications apps were appearing enabled by default, and the home screen showed a Notifications tile that was redundant with the bell icon already in the top bar. User wanted both apps disabled by default and the home tile gone (bell stays). Changes: - scripts/src/seed.ts: flip the seed `isActive` for `notifications` and `calendar` from true → false. The seed inserts apps with `db.insert(appsTable).values(apps).onConflictDoNothing()`, so this is fully idempotent: existing environments (where the row already exists with isActive=true) are NOT changed; only fresh inserts on new installs pick up the disabled default. Admins can enable either app from app settings when they want it. - artifacts/tx-os/src/pages/home.tsx: remove "notifications" from the `dockApps` filter so the bottom dock no longer renders a Notifications tile. Filter becomes `["services", "admin"]`. The top-bar bell icon (and its unread badge) was untouched and still routes to `/notifications`. Out of scope (per spec): no migration to disable existing prod rows; no role/permission changes; bell icon in the top bar stays. Files: - scripts/src/seed.ts (notifications block ~L229, calendar block ~L275) - artifacts/tx-os/src/pages/home.tsx:532 |
||
|
|
c40384b0d9 |
#570: stronger selected-row highlight + Edit button in Quick Actions
Task: on iPad the bulk-selected row was nearly invisible (only a 2px inset navy ring), and the row-tap Quick Actions dialog had only a Postpone button — user asked for a real visible selection fill and an Edit (pencil) action wired to the existing edit-meeting flow. Changes (artifacts/tx-os/src/pages/executive-meetings.tsx): - MeetingRow: add `selectionBg = rgba(11,30,63,0.18)` painted on the row's `<tr>` backgroundColor in addition to the existing `selectionShadow` 2px navy ring. Composition order in trStyle is `quickOpenBg ?? selectionBg ?? baseBg` so an open quick-actions surface still wins (transient action state), user-picked row colours still show when nothing is selected, and the navy tint reads clearly on iPad against the table grid. - MeetingRow: new optional `onQuickEdit?: () => void` prop. Quick Actions Dialog gains a pencil-icon "Edit" button before the existing Postpone button, wrapped in `flex flex-wrap gap-2` so the two buttons sit side-by-side on iPad/desktop and stack on narrow viewports. Button only renders when a handler is wired (defensive — `quickActionsCanMutate` already gates the surface). - ScheduleSection: host a local `editingMeeting` + `editingSaving` state plus a `MeetingFormDialog` mount, with a save handler whose body mirrors ManageSection's `save()` exactly (same PATCH payload projection, same `wrapAsParagraph` / attendee shaping, same toast/error translation). Edit button on a row opens this in-place dialog instead of switching the user to the Manage tab. - Locale: add `executiveMeetings.quickActions.edit` = "تعديل" / "Edit". Notes: - No new icon import needed — `Pencil` was already in the lucide import group. - Selection ring (`#0B1E3F`) and tint use the same navy so they read as one cohesive selected state, not competing layers. - Pre-existing `use-push-subscription.ts` TS noise unchanged. |
||
|
|
dc30d75e11 |
Hide redundant clock and navigation elements for president view
Modify executive-meetings.tsx to conditionally hide the HeaderClock and section navigation when isPresidentView is true. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: e1c6509c-638c-4c43-8d4f-4b4a5910fc4a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fNoqq4C Replit-Helium-Checkpoint-Created: true |
||
|
|
21dda3d372 |
#572 president-only focused view for executive meetings
User asked that when the President (`executive_ceo` role) logs into
the Meetings page he sees a clean "presentation" layout: just the
schedule table, an analog clock plus the weekday/date of the
schedule, and nothing else. The Manage / Notifications / Audit /
PDF tabs and the PDF-export shortcut button are noisy for him and
must disappear — but his backend permissions stay intact.
Changes (frontend only — backend role gates untouched):
- New component `components/executive-meetings/analog-clock.tsx`:
small SVG analog clock (hour/minute/second hands, navy + gold),
ticks every second, ~44px default.
- New inner `PresidentClockBar` component in `executive-meetings.tsx`:
renders the analog clock + weekday + **digital time** (ticking
per second, localized) + selected schedule date in one block.
- `pages/executive-meetings.tsx`:
- Derived `isPresidentView` from the `/me` roles: true iff
`executive_ceo` is present AND the user is NOT also `admin`
or `executive_office_manager` (admins keep full UI).
- Effect forces `section = "schedule"` whenever
`isPresidentView` becomes true, so the president can never
land on a now-hidden tab.
- Hides the header PDF-shortcut button and the entire SECTIONS
tab list when `isPresidentView`. The `#em-toolbar-portal` is
kept so the date picker still mounts.
- `ScheduleSection` accepts two new optional props:
`isPresidentView` and `lang`. When `isPresidentView` is on,
the toolbar renders the AnalogClock next to a label showing
`formatWeekday(date, lang, "long")` and `formatDate(date, lang)`
(e.g. "Sunday · May 17, 2026" / "الأحد · 17 مايو 2026"). The
label updates immediately when the date input changes.
- Edit-mode toggle stays gated on `canMutate`, so the president
(who has canMutate) still gets the inline-edit pencil if he
wants it.
Out of scope: backend role changes, applying the focused view to
other roles, manual presentation-mode toggle. Verified via tsc
(only the pre-existing `use-push-subscription` error remains).
|
||
|
|
27384d019a |
#572 president-only focused view for executive meetings
User asked that when the President (`executive_ceo` role) logs into
the Meetings page he sees a clean "presentation" layout: just the
schedule table, an analog clock plus the weekday/date of the
schedule, and nothing else. The Manage / Notifications / Audit /
PDF tabs and the PDF-export shortcut button are noisy for him and
must disappear — but his backend permissions stay intact.
Changes (frontend only — backend role gates untouched):
- New component `components/executive-meetings/analog-clock.tsx`:
small SVG analog clock (hour/minute/second hands, navy + gold),
ticks every second, ~44px default.
- `pages/executive-meetings.tsx`:
- Derived `isPresidentView` from the `/me` roles: true iff
`executive_ceo` is present AND the user is NOT also `admin`
or `executive_office_manager` (admins keep full UI).
- Effect forces `section = "schedule"` whenever
`isPresidentView` becomes true, so the president can never
land on a now-hidden tab.
- Hides the header PDF-shortcut button and the entire SECTIONS
tab list when `isPresidentView`. The `#em-toolbar-portal` is
kept so the date picker still mounts.
- `ScheduleSection` accepts two new optional props:
`isPresidentView` and `lang`. When `isPresidentView` is on,
the toolbar renders the AnalogClock next to a label showing
`formatWeekday(date, lang, "long")` and `formatDate(date, lang)`
(e.g. "Sunday · May 17, 2026" / "الأحد · 17 مايو 2026"). The
label updates immediately when the date input changes.
- Edit-mode toggle stays gated on `canMutate`, so the president
(who has canMutate) still gets the inline-edit pencil if he
wants it.
Out of scope: backend role changes, applying the focused view to
other roles, manual presentation-mode toggle. Verified via tsc
(only the pre-existing `use-push-subscription` error remains).
|
||
|
|
4b151c50a8 |
#568 silent delete for finished orders
User requested that deleting a finished order from My Orders happen
silently — one click on the trash icon, no confirmation dialog, no
success toast. Just click and the card disappears.
Changes:
- Removed the delete-confirm AlertDialog from OrderCard along with
its `confirmDeleteOpen` state. The trash icon now calls
`handleDelete` directly.
- Removed `toast({ title: t("myOrders.deleted") })` from
`scheduleDelete`. The 7-second soft-delete grace window stays in
place (pendingDeleteIds filter + setTimeout) so the API DELETE
still fires after the window. The `deleteFailed` error toast is
preserved so silent API failures don't lose orders.
- Dropped the now-unused locale keys from ar.json and en.json:
`deleteConfirmTitle`, `deleteConfirmBody`, `deleteConfirm`,
`deleted`.
- Removed the unused `AlertDialogDescription` import (the remaining
cancel-confirm dialog uses only the title).
Cancel-order flow remains untouched (still shows confirm dialog and
undo toast as in #566).
|
||
|
|
4c57c58f8c |
#566 clean up My Orders page (3 removals)
Per user request on the طلباتي page: 1. Removed the "Clear finished orders" bulk button entirely. The button was showing as a raw i18n key (myOrder c...) for the user and they asked for it gone. Dropped the related AlertDialog, confirmClearOpen state, handleClearFinished handler, and the locale keys clearFinished_*, clearConfirmTitle/Body_*, clearConfirm, clearedCount_*, and clearedPartial in both ar.json and en.json. 2. Removed the "تراجع / Undo" ToastAction shown after deleting an order. The toast now displays only the "Order deleted" title; the 7-second timer + pendingDeleteIds soft-delete window is preserved so the actual DELETE still fires after the grace period. The undo closure was deleted since nothing references it anymore. Single-id call site stays untouched (scheduleDelete still receives [id]). 3. Removed the "ستتاح لك ثوانٍ قليلة للتراجع" description from the cancel-order confirmation dialog. The cancelConfirmBody key was dropped from both locales. The AlertDialog now shows only the title + the two action buttons. Out of scope (kept as-is): - The undo ToastAction after CANCEL (user only asked about delete). - Incoming-orders page. Notes: - ToastAction import is still used by the cancel-undo path. - Trash2 import still used by the per-card delete button. - Pre-existing TS error in use-push-subscription.ts is unrelated. |
||
|
|
4e5bed602e |
fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on iPad/mobile. Six fixes, all client-side: 1. Quick-actions surface now opens from ANY click on a meeting row (was: only the attendees cell). Root cause: handleRowClick guard skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*` descendants. Those affordances only do anything in edit mode, but handleRowClick already early-returns when edit mode is on, so the testid guard was always over-aggressive. Dropped those guards plus the `[role='button']` rule from the interactive selector, kept the real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc. guards and the `em-row-actions-*` / `em-row-select-*` testid guards (those affordances are visible outside edit mode and must not trigger the surface). File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350). 2. Originating row is now unmistakable on iPad: quickOpenShadow is `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single 3px inset) and the row background tints to `rgba(highlight, 0.08)` while quick-actions is open. File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4287, L4344). 3. Postpone dialog header was overlapping the Radix close X in RTL. Added `pr-8` on the DialogHeader (same pattern already used for the row-quick mini-dialog). File: artifacts/tx-os/src/components/executive-meetings/ upcoming-meeting-alert.tsx (~L1447). 4. Reschedule tab: date / start / end inputs collided on iPad portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column through iPad portrait, three across on landscape/desktop), with `min-w-0` on each cell and `w-full` on every Input so Safari's native date/time pickers stop overflowing. Same file (~L1761). 5. Cancel tab X overlap is resolved by the same DialogHeader fix in step 3 (one Dialog wraps all three tabs). 6. Removed the `cancelHint` paragraph + the `cancelHint` translation keys in ar.json (L1252) and en.json (L1164) — fully unused now. NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky). Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this). |
||
|
|
620ab284e2 |
fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on iPad/mobile. Six fixes, all client-side: 1. Quick-actions surface now opens from ANY click on a meeting row (was: only the attendees cell). Root cause: handleRowClick guard skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*` descendants. Those affordances only do anything in edit mode, but handleRowClick already early-returns when edit mode is on, so the testid guard was always over-aggressive. Dropped those guards plus the `[role='button']` rule from the interactive selector, kept the real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc. guards and the `em-row-actions-*` / `em-row-select-*` testid guards (those affordances are visible outside edit mode and must not trigger the surface). File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350). 2. Originating row is now unmistakable on iPad: quickOpenShadow is `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single 3px inset) and the row background tints to `rgba(highlight, 0.08)` while quick-actions is open. File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4287, L4344). 3. Postpone dialog header was overlapping the Radix close X in RTL. Added `pr-8` on the DialogHeader (same pattern already used for the row-quick mini-dialog). File: artifacts/tx-os/src/components/executive-meetings/ upcoming-meeting-alert.tsx (~L1447). 4. Reschedule tab: date / start / end inputs collided on iPad portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column through iPad portrait, three across on landscape/desktop), with `min-w-0` on each cell and `w-full` on every Input so Safari's native date/time pickers stop overflowing. Same file (~L1761). 5. Cancel tab X overlap is resolved by the same DialogHeader fix in step 3 (one Dialog wraps all three tabs). 6. Removed the `cancelHint` paragraph + the `cancelHint` translation keys in ar.json (L1252) and en.json (L1164) — fully unused now. NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky). Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this). |
||
|
|
0f71ae4102 |
feat(executive-meetings): grant executive_ceo mutate + audit + push notify (Task #561)
User request: السماح للرئيس (executive_ceo) بإنشاء/تعديل/تأجيل/حذف
الاجتماعات، استلام إشعارات Push للتذكير، والاطلاع على سجل التدقيق —
مع إبقاء إعدادات الخطوط/الشعار حصرية على مدير المكتب التنفيذي.
Changes (3 lines added, backend-only):
1. artifacts/api-server/src/routes/executive-meetings.ts
- Added "executive_ceo" to MUTATE_ROLES (line 120) → POST/PATCH/DELETE
on meetings now accepted for the CEO role.
- Added "executive_ceo" to ADMIN_AUDIT_ROLES (line 130) → /audit
endpoint now reachable; canViewAudit=true in the bootstrap payload.
- EM_ADMIN_ROLES intentionally untouched, so font-settings and global
PDF brand assets remain admin + executive_office_manager only.
2. artifacts/api-server/src/lib/executive-meeting-scheduler.ts
- Added "executive_ceo" to EM_NOTIFY_ROLES (line 18) → CEO users with
an active Web Push subscription now receive the LEAD_MINUTES=5
pre-meeting reminder push.
Frontend requires no changes: it gates buttons on canMutate /
canViewAudit booleans returned by the bootstrap endpoint (lines
617-619), which flip to true automatically for the CEO role.
Verification: API workflow restarted clean, scheduler started OK,
/api/executive-meetings and /alert-state respond 304/200.
NOT pushed to Gitea: Tailscale link to desktop-11cj93j is still down
from this sandbox (HTTP=000). Task #560's earlier VAPID fix
(
|
||
|
|
629069b1fe |
Fix VAPID subject fallback for push notifications
Update push notification logic to correctly handle missing VAPID_SUBJECT environment variables by using logical OR operator instead of nullish coalescing for subject fallback. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 96637a28-4f37-4966-8adb-95f8f66ee769 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fNoqq4C Replit-Helium-Checkpoint-Created: true |
||
|
|
51c3d8a8ba |
Fix toggle switch behavior in right-to-left reading modes
Update the switch component's CSS to correctly position the toggle thumb in RTL layouts for both checked and unchecked states. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: ee6229a6-5472-4ca0-8f84-74b229703438 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX Replit-Helium-Checkpoint-Created: true |
||
|
|
f4253d4d9d |
Improve toggle switch behavior in right-to-left languages
Add custom RTL and LTR variants to Tailwind CSS configuration to correctly position toggle switch indicators in Arabic and other right-to-left languages. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 84451adc-9c86-40d5-80ba-846e7ea2a813 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX Replit-Helium-Checkpoint-Created: true |
||
|
|
05446afb5d |
Improve meeting reminders with new scheduler and improved push notifications
Add a new background scheduler to trigger meeting reminders, implement atomic claims for push notifications to prevent duplicates, and add an `ignoreConnected` option to push notifications. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: a63187bd-4947-4e1f-8959-e8d68ff96a8c Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/97ZFaoX Replit-Helium-Checkpoint-Created: true |
||
|
|
55d19298e7 |
Add bilingual support for notifications and automatic HTTPS
Implement bilingual text for push notifications, allowing users to receive them in their preferred language. Automatically configure HTTPS with Let's Encrypt for custom domains to ensure persistent subscriptions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 94f06aee-a5cf-45df-979e-f940cce77214 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/yEFtMKN Replit-Helium-Checkpoint-Created: true |
||
|
|
d78818b88a |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes. - **De-dup gate:** skips push when the user has any active Socket.IO connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a connected user only gets the in-app chime, never a duplicate system notification. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (shared device) so the previous user's notifications can't leak. - Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe`, and a spec-aligned alias `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated). - Push hooked into 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick only (no asset caching). All URLs (icon, badge, navigation target) resolved against `self.registration.scope`, so the SW works at root or under a subpath without code changes. - SW registration in `main.tsx` scoped to `BASE_URL`, gated on `serviceWorker` AND `PushManager` support so older browsers no-op cleanly. - `use-push-subscription` hook (enable/disable/refresh + status). - New `PushEnablePrompt` card mounted in `App` — appears on first launch when supported + permission still "default", one-tap enable, dismiss persists for 14 days. Silent on unsupported devices. - `PushToggleRow` added inside Notification Settings. - ar/en strings: `notifSettings.push.*` and `common.later`. Plumbing - OpenAPI: 3 new operations under `notifications`; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the key; subscribe endpoint 401s without auth. - New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing — covers VAPID endpoint, auth gating on subscribe/unsubscribe, row persistence + removal, idempotent re-subscribe with key rotation, account-switch endpoint reassignment, and malformed-input rejection. - New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3 passing — true in-process unit test (`node --import tsx --experimental-test-module-mocks`) that mocks `web-push` to verify the prune path: 410 deletes the row, 404 deletes the row, and a transient 500 leaves the row intact. `tsx` added as a devDep and the test:run script picks up `*.test.ts` alongside the existing `.mjs` suites. - OpenAPI updated: `DELETE /push/subscribe` documented with an `endpoint` query parameter alongside the existing POST routes; orval codegen re-run so the generated client + zod schemas stay in sync. - Restored the unrelated serial tests (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`) that an earlier cleanup pass dropped — they are unchanged from prior green state. - Architect rounds addressed: 1. race + payload size. 2. dedup gate + first-launch UX + SW base-path + initial tests. 3. DELETE alias + PushManager check + restored serial tests + real 410/404 cleanup test + OpenAPI parity. 4. .env.docker.example + replit.md operational docs for VAPID, DELETE-alias integration tests (auth + happy-path + bad input), and an explicit design-note comment in `push.ts` documenting the intentional user-wide dedup policy. |
||
|
|
0740971a96 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes. - **De-dup gate:** skips push when the user has any active Socket.IO connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a connected user only gets the in-app chime, never a duplicate system notification. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (shared device) so the previous user's notifications can't leak. - Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe`, and a spec-aligned alias `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated). - Push hooked into 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick only (no asset caching). All URLs (icon, badge, navigation target) resolved against `self.registration.scope`, so the SW works at root or under a subpath without code changes. - SW registration in `main.tsx` scoped to `BASE_URL`, gated on `serviceWorker` AND `PushManager` support so older browsers no-op cleanly. - `use-push-subscription` hook (enable/disable/refresh + status). - New `PushEnablePrompt` card mounted in `App` — appears on first launch when supported + permission still "default", one-tap enable, dismiss persists for 14 days. Silent on unsupported devices. - `PushToggleRow` added inside Notification Settings. - ar/en strings: `notifSettings.push.*` and `common.later`. Plumbing - OpenAPI: 3 new operations under `notifications`; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the key; subscribe endpoint 401s without auth. - New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing — covers VAPID endpoint, auth gating on subscribe/unsubscribe, row persistence + removal, idempotent re-subscribe with key rotation, account-switch endpoint reassignment, and malformed-input rejection. - New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3 passing — true in-process unit test (`node --import tsx --experimental-test-module-mocks`) that mocks `web-push` to verify the prune path: 410 deletes the row, 404 deletes the row, and a transient 500 leaves the row intact. `tsx` added as a devDep and the test:run script picks up `*.test.ts` alongside the existing `.mjs` suites. - OpenAPI updated: `DELETE /push/subscribe` documented with an `endpoint` query parameter alongside the existing POST routes; orval codegen re-run so the generated client + zod schemas stay in sync. - Restored the unrelated serial tests (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`) that an earlier cleanup pass dropped — they are unchanged from prior green state. - Architect rounds addressed: 1. race + payload size. 2. dedup gate + first-launch UX + SW base-path + initial tests. 3. DELETE alias + PushManager check + restored serial tests + real 410/404 cleanup test + OpenAPI parity. |
||
|
|
bf39a6eda6 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes. - **De-dup gate:** skips push when the user has any active Socket.IO connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a connected user only gets the in-app chime, never a duplicate system notification. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (shared device) so the previous user's notifications can't leak. - Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe`, and a spec-aligned alias `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated). - Push hooked into 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick only (no asset caching). All URLs (icon, badge, navigation target) resolved against `self.registration.scope`, so the SW works at root or under a subpath without code changes. - SW registration in `main.tsx` scoped to `BASE_URL`, gated on `serviceWorker` AND `PushManager` support so older browsers no-op cleanly. - `use-push-subscription` hook (enable/disable/refresh + status). - New `PushEnablePrompt` card mounted in `App` — appears on first launch when supported + permission still "default", one-tap enable, dismiss persists for 14 days. Silent on unsupported devices. - `PushToggleRow` added inside Notification Settings. - ar/en strings: `notifSettings.push.*` and `common.later`. Plumbing - OpenAPI: 3 new operations under `notifications`; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the key; subscribe endpoint 401s without auth. - New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing — covers VAPID endpoint, auth gating on subscribe/unsubscribe, row persistence + removal, idempotent re-subscribe with key rotation, account-switch endpoint reassignment, and malformed-input rejection. - Architect rounds addressed in order: 1. race + payload size — fixed. 2. dedup gate + first-launch UX + SW base-path + tests — fixed. 3. DELETE alias + PushManager check — fixed in this commit. A true 410-cleanup test was attempted but reaching the prune branch from an out-of-process .mjs test requires mocking the `web-push` module, which the current `node --test` + bundled-dist harness doesn't support cleanly; the 404/410 catch branch is small, self-contained, and was code-reviewed. Tracked as a follow-up. |
||
|
|
8f3b750961 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes. - **De-dup gate:** skips push when the user has any active Socket.IO connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a connected user only gets the in-app chime, never a duplicate system notification. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (shared device) so the previous user's notifications can't leak. - Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe` (auth-gated, Zod-validated). - Push hooked into 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick only (no asset caching). All URLs (icon, badge, navigation target) resolved against `self.registration.scope`, so the SW works at root or under a subpath without code changes. - SW registration in `main.tsx` scoped to `BASE_URL`. - `use-push-subscription` hook (enable/disable/refresh + status). - New `PushEnablePrompt` card mounted in `App` — appears on first launch when supported + permission still "default", one-tap enable, dismiss persists for 14 days. Silent on unsupported devices. - `PushToggleRow` added inside Notification Settings. - ar/en strings: `notifSettings.push.*` and `common.later`. Plumbing - OpenAPI: 3 new operations under `notifications`; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the key; subscribe endpoint 401s without auth. - New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing — covers VAPID endpoint, auth gating on subscribe/unsubscribe, row persistence + removal, idempotent re-subscribe with key rotation, account-switch endpoint reassignment, and malformed-input rejection. - Two architect rounds: first flagged race + payload size (fixed), second flagged dedup gate + first-launch UX + SW base-path + tests (all fixed in this commit). |
||
|
|
243ccb3e00 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral in-memory. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes so over-sized notes don't kill delivery. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (account switch on shared device) so the previous user's notifications can't leak. - Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe` (auth-gated, Zod-validated). - Push hooked into the 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only (no asset caching). Focuses an existing tab or opens a new one at the payload URL. - SW registration in `main.tsx` scoped to `BASE_URL`. - `use-push-subscription` hook (enable/disable/refresh + status: unsupported / denied / default / subscribed). - New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings (notifSettings.push.*). iPad copy explains that the user must add to Home Screen first for iOS to allow Web Push. Plumbing - OpenAPI: 3 new operations under `notifications` tag; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the generated key; subscribe endpoint 401s without auth as expected. - Architect review surfaced two HIGH issues (account-switch leak, payload size); both fixed before completion. |
||
|
|
e9d7b8dc76 |
Fix service images + Arabic ي dots on home tiles (Task #552)
Two visible bugs reported from the iPad after Task #551 deploy: 1) Only "قهوة سعودي" showed its image on /services. Three other seeded services (شاي، بلاك كوفي، عصير طازج) showed a broken-image placeholder because the seed only set `imageUrl` for Saudi Coffee. 2) The dots under final ي in the home-grid tile label "خدماتي" didn't render on iOS Safari. The label `<span>` didn't pin a font-family, so iOS fell back to a Latin face that lacks proper Arabic glyphs. Changes: - scripts/src/seed.ts: • Added `imageUrl` for tea/black-coffee/juice in the insert array (covers fresh installs). • Added a backfill loop AFTER the insert that fills `imageUrl` on existing rows only when the value is currently NULL. Admin- customized image URLs are never overwritten — matches the existing legacy-rename pattern at lines 395-423. - artifacts/tx-os/src/pages/home.tsx (AppIconContent label): • Added `lang` and `dir` attributes on the tile `<span>`. • Pinned an explicit font stack: DIN Next LT Arabic → Helvetica Neue LT Arabic → Tajawal for Arabic; Helvetica Neue → system-ui for English. The three Arabic faces are already declared in custom-fonts.css. No schema changes, no new dependencies. Verified typecheck passes and HMR reloaded both files cleanly in the dev server. Push to Gitea is a separate follow-up (git commit is restricted in the main agent; platform commits this task's changes on completion, and the push task can then mirror them to the Mac). |
||
|
|
a6d5988421 |
Add PWA manifest + icons so iPad opens Tx OS full-screen (Task #550)
Problem: on the iPad the Safari URL bar stayed visible even after
"Add to Home Screen" because index.html had no PWA manifest and no
Apple PWA meta tags — the home-screen icon opened a normal Safari tab.
Changes:
- New `artifacts/tx-os/public/manifest.webmanifest`:
name/short_name "Tx OS", start_url "/", scope "/",
display "standalone", background_color and theme_color "#0B1E3F"
(matches the Meetings brand), and 3 icons (192/512 "any" + 512
"maskable" with safe-area padding).
- Generated icon PNGs from the existing `public/favicon.svg` brand
mark via ImageMagick:
public/icons/icon-192.png (192x192)
public/icons/icon-512.png (512x512)
public/icons/icon-maskable-512.png (512x512, 360x360 logo
centered on #0B1E3F so iOS/
Android masks don't crop)
public/apple-touch-icon.png (180x180, iPad/iPhone home)
- `artifacts/tx-os/index.html` head: added <link rel="manifest">,
<link rel="apple-touch-icon">, plus apple-mobile-web-app-capable,
mobile-web-app-capable, apple-mobile-web-app-status-bar-style
(default), apple-mobile-web-app-title, and theme-color meta tags.
- `replit.md`: new "iPad / iPhone — open Tx OS full-screen" section
with the one-time Add-to-Home-Screen steps and the gotcha that
pre-existing shortcuts must be removed and re-added to pick up the
manifest.
Verified: dev server returns 200 for /manifest.webmanifest,
/apple-touch-icon.png, and /icons/icon-192.png. The new files all
live under public/ so they're copied verbatim into the nginx image
on the next docker build.
|
||
|
|
66e0e5697f |
Task #542: fix api-server test deadlocks/races (all 327+13 tests green)
- executive-meetings.ts: add lockMeetingDate(executor, date)
pg_advisory_xact_lock helper. Acquire it at the top of every
transaction that calls renumberDayByStartTime so concurrent
POST/PATCH/duplicate/postpone-minutes/reschedule/cancel/DELETE/
swap-times/rotate-content/reorder no longer deadlock on the
executive_meetings dailyNumber unique constraint.
- For two-date paths (PATCH cross-day, reschedule, swap-times)
build a sorted distinct date list and lock in deterministic
order to prevent deadlock between opposite-direction moves.
- executive-meeting-notify.ts: add `.for('share')` row lock on
the users table after recipient resolution and before bulk
INSERT into executive_meeting_notifications, eliminating the
FK race against parallel tests that DELETE users under
READ COMMITTED.
- Move tests/executive-meetings-notifications.test.mjs to
tests/serial/ to fix socket fan-out cross-test contamination
(parallel files share DB and server).
No tests were weakened or skipped; all fixes live in
route/service code.
|
||
|
|
a5a35ef217 |
Improve test execution by automatically starting and stopping the API server
Refactor test execution to use a new script that manages the API server lifecycle, including building, starting, waiting for health, running tests, and shutting down, with support for using an existing server. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: a42bd3b2-3145-4118-87aa-336a7e2189ca Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/FvHcc7z Replit-Helium-Checkpoint-Created: true |
||
|
|
77f8096deb |
Improve security and deployment flexibility for external access
Update README and application configurations to support reverse proxy setups, including TLS termination and proper cookie handling, and add an option to seed demo meetings. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: c15da2be-cee7-4cbb-8d58-f9fcc3c38ed7 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/FvHcc7z Replit-Helium-Checkpoint-Created: true |
||
|
|
fa31fb6374 |
task #539: push deployment-hardening branch to Gitea
Pushed the existing main-branch commits (since |
||
|
|
f16f4763df |
Improve handling of external proxy configurations and session security
Introduces `TRUST_PROXY_HTTPS` environment variable and updates session cookie security logic in `app.ts`. Modifies `README.md` and `docker-compose.yml` for clarity on proxy configurations. Updates `seed.ts` to conditionally seed demo meetings based on `SEED_DEMO_MEETINGS` environment variable. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 097c59e1-8bbe-4846-a79e-3e661e6645c4 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/FvHcc7z Replit-Helium-Checkpoint-Created: true |
||
|
|
ca1508b04f |
feat(setup): Stage 1 first-time setup wizard backend (no UI)
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535). Backend - New system_settings table (id=1 singleton): installed flag, base_url, local_domain, local_ip, https_mode, app_version. Pushed to dev DB. - New /api/setup/status (open) and /api/setup/{validate,complete} (gated by requireSetupOpen — 409 once installed). - completeInstall is fully transactional: pg_advisory_xact_lock serializes concurrent callers, double-gates on installed flag and admin existence, then atomically creates the admin user, assigns admin role + Admins/Everyone groups, and flips system_settings to installed=true. Rolls back on any failure. - Added redirectIfSetupNeeded() helper returning the full SetupStatus payload alongside a redirect target for SPA routing decisions. - Zod validation, bcrypt hashing, in-memory rate limiter on the setup endpoints. Backward compat - scripts/src/seed.ts now branches on installed flag + admin existence + SEED_*_PASSWORD env vars. Legacy installs (admin exists, system_settings empty) get backfilled to installed=true via ON CONFLICT DO UPDATE so they are never forced through the wizard. When env passwords are unset and no admin exists, the seed prints a wizard hint instead of seeding. Infra - docker-compose.yml: replaced nginx edge with a Caddy service that mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no longer publishes a port directly — Caddy is the only public ingress. - Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when HTTPS_MODE=skip so a fresh host without mkcert can still boot. - docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with WebSocket upgrade preserved and an HTTP→HTTPS redirect. - start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so the legacy http://localhost:${APP_PORT} URL keeps working. In local/byo mode it prints the https://${LOCAL_DOMAIN} URL. - .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT, HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional. Tooling - scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert, mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN). Tests - artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass. - scripts/tests/local-setup.test.mjs: 2/2 pass. Constraints honored: no force-push, no destructive ops, start.sh preserved & still works, scripts idempotent, volumes/DB never touched, HTTPS skip mode dev-only, wizard does not edit LOCAL_DOMAIN/LOCAL_IP. Out of scope / not addressed: pre-existing TS errors in routes/users.ts and pre-existing failure in executive-meetings-postpone-race.test.mjs. |
||
|
|
dba320fe35 |
feat(setup): Stage 1 first-time setup wizard backend (no UI)
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535). Backend - New system_settings table (id=1 singleton): installed flag, base_url, local_domain, local_ip, https_mode, app_version. Pushed to dev DB. - New /api/setup/status (open) and /api/setup/{validate,complete} (gated by requireSetupOpen — 409 once installed). - completeInstall is fully transactional: pg_advisory_xact_lock serializes concurrent callers, double-gates on installed flag and admin existence, then atomically creates the admin user, assigns admin role + Admins/Everyone groups, and flips system_settings to installed=true. Rolls back on any failure. - Added redirectIfSetupNeeded() helper returning the full SetupStatus payload alongside a redirect target for SPA routing decisions. - Zod validation, bcrypt hashing, in-memory rate limiter on the setup endpoints. Backward compat - scripts/src/seed.ts now branches on installed flag + admin existence + SEED_*_PASSWORD env vars. Legacy installs (admin exists, system_settings empty) get backfilled to installed=true via ON CONFLICT DO UPDATE so they are never forced through the wizard. When env passwords are unset and no admin exists, the seed prints a wizard hint instead of seeding. Infra - docker-compose.yml: replaced nginx edge with a Caddy service that mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no longer publishes a port directly — Caddy is the only public ingress. - Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when HTTPS_MODE=skip so a fresh host without mkcert can still boot. - docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with WebSocket upgrade preserved and an HTTP→HTTPS redirect. - start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so the legacy http://localhost:${APP_PORT} URL keeps working. In local/byo mode it prints the https://${LOCAL_DOMAIN} URL. - .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT, HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional. Tooling - scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert, mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN). Tests - artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass. - scripts/tests/local-setup.test.mjs: 2/2 pass. Constraints honored: no force-push, no destructive ops, start.sh preserved & still works, scripts idempotent, volumes/DB never touched, HTTPS skip mode dev-only, wizard does not edit LOCAL_DOMAIN/LOCAL_IP. Out of scope / not addressed: pre-existing TS errors in routes/users.ts and pre-existing failure in executive-meetings-postpone-race.test.mjs. |