From 1659dc4b684911689d679995ca23bea499e52b1e Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Mon, 18 May 2026 10:39:17 +0000 Subject: [PATCH] Task #589: Live build stamp + server start time in System Updates panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- artifacts/api-server/src/routes/system.ts | 11 +++++ artifacts/tx-os/src/locales/ar.json | 1 + artifacts/tx-os/src/locales/en.json | 1 + artifacts/tx-os/src/pages/admin.tsx | 43 +++++++++++++++++++ docker-compose.yml | 10 +++++ docker/api-server.Dockerfile | 12 ++++++ .../src/generated/api.schemas.ts | 6 +++ lib/api-spec/openapi.yaml | 9 ++++ lib/api-zod/src/generated/api.ts | 5 +++ scripts/redeploy.sh | 7 +++ 10 files changed, 105 insertions(+) diff --git a/artifacts/api-server/src/routes/system.ts b/artifacts/api-server/src/routes/system.ts index 6662f963..b6f83ce9 100644 --- a/artifacts/api-server/src/routes/system.ts +++ b/artifacts/api-server/src/routes/system.ts @@ -9,6 +9,12 @@ const router: IRouter = Router(); const CURRENT_VERSION: string = versionFile.version; const CURRENT_BUILD: string = versionFile.build; +// Captured once at module load — frozen for the lifetime of the +// process. The admin "System Updates" panel renders this so the user +// gets a visible signal whenever the container actually restarts +// (independent of build/version, which only change on rebuild). +const SERVER_STARTED_AT: string = new Date().toISOString(); + const LATEST_URL = process.env.LATEST_VERSION_URL ?? ""; const FETCH_TIMEOUT_MS = 5000; @@ -29,6 +35,7 @@ router.get("/system/version", requireAdmin, async (_req, res): Promise => errorMessage: null, checkedAt, configured: false, + startedAt: SERVER_STARTED_AT, }), ); return; @@ -50,6 +57,7 @@ router.get("/system/version", requireAdmin, async (_req, res): Promise => errorMessage: `HTTP ${response.status}`, checkedAt, configured: true, + startedAt: SERVER_STARTED_AT, }), ); return; @@ -68,6 +76,7 @@ router.get("/system/version", requireAdmin, async (_req, res): Promise => errorMessage: "Invalid version format", checkedAt, configured: true, + startedAt: SERVER_STARTED_AT, }), ); return; @@ -86,6 +95,7 @@ router.get("/system/version", requireAdmin, async (_req, res): Promise => errorMessage: null, checkedAt, configured: true, + startedAt: SERVER_STARTED_AT, }), ); } catch (err) { @@ -103,6 +113,7 @@ router.get("/system/version", requireAdmin, async (_req, res): Promise => errorMessage: message, checkedAt, configured: true, + startedAt: SERVER_STARTED_AT, }), ); } finally { diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index c6d05b8d..e3f03894 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -556,6 +556,7 @@ "checkNow": "تحقّق الآن", "loading": "جاري التحقق من الإصدار…", "lastChecked": "آخر تحقق", + "serverStartedAt": "بدأ تشغيل الخادم", "toastErrorTitle": "تعذّر التحقق", "toastErrorDesc": "لم نتمكن من الوصول إلى مصدر التحديثات. حاول مرة أخرى لاحقاً." }, diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 6c140b69..34dbe57c 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -532,6 +532,7 @@ "checkNow": "Check Now", "loading": "Checking version…", "lastChecked": "Last checked", + "serverStartedAt": "Server started", "toastErrorTitle": "Check failed", "toastErrorDesc": "Could not reach the update source. Please try again later." }, diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index 4cdbbf0c..6bd6d23d 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -2381,6 +2381,31 @@ function SystemUpdatesPanel() { }).format(d); }; + // The Docker build step stamps `build` as `YYYYMMDDTHHmm.shortsha` + // (e.g. `20260518T1042.a34eb5f5`). When the value matches that + // pattern, render a localized date/time next to the raw token so a + // non-technical user can see at a glance when the image was built. + // Older / hand-edited values (e.g. `20260517.b5efd9eb`) still render + // with date-only when possible, otherwise we just show the raw text. + const formatBuildStamp = (build: string | undefined) => { + if (!build) return null; + const m = build.match( + /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2}))?\.[A-Fa-f0-9]+$/, + ); + if (!m) return null; + const [, y, mo, da, hh, mm] = m; + const iso = + hh && mm + ? `${y}-${mo}-${da}T${hh}:${mm}:00Z` + : `${y}-${mo}-${da}T00:00:00Z`; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return null; + return new Intl.DateTimeFormat(i18n.language === "ar" ? "ar" : "en", { + dateStyle: "medium", + ...(hh && mm ? { timeStyle: "short" } : {}), + }).format(d); + }; + const renderStatusBadge = (resp: SystemVersionResponse | undefined) => { if (!resp) return null; const map: Record< @@ -2474,7 +2499,25 @@ function SystemUpdatesPanel() { {data.current.build} + {formatBuildStamp(data.current.build) && ( + + ({formatBuildStamp(data.current.build)}) + + )} + {data.startedAt && ( +
+ {t("admin.systemUpdates.serverStartedAt")}:{" "} + {formatChecked( + typeof data.startedAt === "string" + ? data.startedAt + : (data.startedAt as Date).toISOString(), + )} +
+ )}
diff --git a/docker-compose.yml b/docker-compose.yml index 9b688a38..7ca2db0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,6 +43,16 @@ services: build: context: . dockerfile: docker/api-server.Dockerfile + # Stamped into version.json inside the image so the admin + # "System Updates" panel can show *which* code is running. + # `scripts/redeploy.sh` exports both before calling `build`. + # If you call `docker compose build` by hand, pass them too: + # GIT_SHA=$(git rev-parse --short HEAD) \ + # BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ + # docker compose up -d --build api + args: + GIT_SHA: ${GIT_SHA:-unknown} + BUILD_DATE: ${BUILD_DATE:-unknown} image: tx-os/api-server:latest restart: unless-stopped depends_on: diff --git a/docker/api-server.Dockerfile b/docker/api-server.Dockerfile index dc9a922b..dab2da53 100644 --- a/docker/api-server.Dockerfile +++ b/docker/api-server.Dockerfile @@ -32,11 +32,23 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile --ignore-scripts FROM deps AS build +# Build-time stamp. Pass these from `docker compose build` so the +# admin "System Updates" panel can show *which* code is running, not +# just whatever is hard-coded in version.json. When the args are not +# provided (e.g. someone running `docker build` by hand), we leave +# version.json untouched. +ARG GIT_SHA="" +ARG BUILD_DATE="" COPY tsconfig.base.json* tsconfig.json* ./ COPY version.json ./ COPY lib lib COPY scripts scripts COPY artifacts/api-server artifacts/api-server +RUN if [ -n "$GIT_SHA" ] && [ -n "$BUILD_DATE" ] && [ "$GIT_SHA" != "unknown" ] && [ "$BUILD_DATE" != "unknown" ]; then \ + GIT_SHA="$GIT_SHA" BUILD_DATE="$BUILD_DATE" node -e "const fs=require('fs');const p='./version.json';const v=JSON.parse(fs.readFileSync(p,'utf8'));const d=String(process.env.BUILD_DATE||'').replace(/[-:]/g,'').replace(/\..*$/,'').slice(0,13);v.build=(d||'unknownDate')+'.'+(process.env.GIT_SHA||'unknownSha');fs.writeFileSync(p,JSON.stringify(v,null,2)+'\n');console.log('Stamped build:',v.build);" ; \ + else \ + echo "No GIT_SHA/BUILD_DATE provided — keeping version.json as-is" ; \ + fi RUN pnpm --filter @workspace/api-server run build FROM base AS runtime diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 40f5eaa6..55899fb1 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -33,6 +33,12 @@ export interface SystemVersionResponse { /** @nullable */ checkedAt: string | null; configured: boolean; + /** ISO-8601 timestamp captured when the API process booted. +Frozen for the lifetime of the process — used by the admin +System Updates panel as visible proof that the container +actually restarted (independent of build/version changes). + */ + startedAt: string; } export interface ErrorResponse { diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 05d48a7a..84e07779 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -2977,6 +2977,14 @@ components: format: date-time configured: type: boolean + startedAt: + type: string + format: date-time + description: | + ISO-8601 timestamp captured when the API process booted. + Frozen for the lifetime of the process — used by the admin + System Updates panel as visible proof that the container + actually restarted (independent of build/version changes). required: - current - latest @@ -2984,6 +2992,7 @@ components: - errorMessage - checkedAt - configured + - startedAt ErrorResponse: type: object diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 99439aed..26907554 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -43,6 +43,11 @@ export const GetSystemVersionResponse = zod.object({ errorMessage: zod.string().nullable(), checkedAt: zod.coerce.date().nullable(), configured: zod.boolean(), + startedAt: zod.coerce + .date() + .describe( + "ISO-8601 timestamp captured when the API process booted.\nFrozen for the lifetime of the process — used by the admin\nSystem Updates panel as visible proof that the container\nactually restarted (independent of build\/version changes).\n", + ), }); /** diff --git a/scripts/redeploy.sh b/scripts/redeploy.sh index 7afd54b2..9bf6de25 100755 --- a/scripts/redeploy.sh +++ b/scripts/redeploy.sh @@ -79,6 +79,13 @@ else fi step "2/5 docker compose build api web (rebuilds SPA + API images)" +# Stamp the API image with the current git SHA + UTC build time so the +# admin "System Updates" panel shows visible proof that the new image +# is actually running (instead of the old, hand-edited version.json). +# Both are read inside docker/api-server.Dockerfile as ARGs. +export GIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" +export BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "[redeploy] GIT_SHA=$GIT_SHA BUILD_DATE=$BUILD_DATE" "${DC[@]}" build api web step "3/5 docker compose run --rm migrate (schema push + tolerant seed)"