@@ -20,6 +20,7 @@ STATIC_FRONTEND="${REPO_ROOT}/hpcperfstats/site/hpcperfstats_site/static/fronten
2020CONTAINER_STATIC_FRONTEND=" /home/hpcperfstats/hpcperfstats/site/hpcperfstats_site/static/frontend"
2121CONTAINER_STATIC_ROOT=" ${CONTAINER_STATIC_ROOT:-/ home/ hpcperfstats/ staticfiles} "
2222CONTAINER_STATIC_ROOT_FRONTEND=" ${CONTAINER_STATIC_ROOT} /frontend"
23+ PROXY_STATIC_ROOT_FRONTEND=" /srv/static/frontend"
2324NODE_IMAGE=" node:26.3.0-alpine3.23"
2425
2526# Next static export shells nginx serves (see services-conf/nginx-static-files.conf).
@@ -148,6 +149,7 @@ build_in_docker() {
148149
149150verify_build_output () {
150151 verify_spa_shells " ${STATIC_FRONTEND} "
152+ print_deploy_fingerprint " host build" " ${STATIC_FRONTEND} /machine/index.html"
151153 echo " Built frontend static export: ${STATIC_FRONTEND} "
152154}
153155
@@ -179,6 +181,56 @@ file_sha256() {
179181 fi
180182}
181183
184+ frontend_build_fingerprint () {
185+ local index_html=" $1 "
186+ if [[ ! -f " ${index_html} " ]]; then
187+ echo " unknown"
188+ return
189+ fi
190+ local fingerprint
191+ fingerprint=" $( grep -oE ' page-[0-9a-f]+\.js' " ${index_html} " | head -n 1 || true) "
192+ if [[ -n " ${fingerprint} " ]]; then
193+ echo " ${fingerprint} "
194+ return
195+ fi
196+ grep -oE ' <!--[^>]{8,}-->' " ${index_html} " | head -n 1 | tr -d ' <!->' || echo " unknown"
197+ }
198+
199+ print_deploy_fingerprint () {
200+ local label=" $1 "
201+ local index_html=" $2 "
202+ local fingerprint
203+ fingerprint=" $( frontend_build_fingerprint " ${index_html} " ) "
204+ echo " Deploy fingerprint (${label} ): ${fingerprint} "
205+ }
206+
207+ print_container_deploy_fingerprint () {
208+ local label=" $1 "
209+ local service=" $2 "
210+ local container_path=" $3 "
211+ local fingerprint
212+ fingerprint=" $(
213+ docker compose exec -T " ${service} " sh -lc \
214+ " grep -oE 'page-[0-9a-f]+\\ .js' '${container_path} ' 2>/dev/null | head -n 1 || echo unknown"
215+ ) "
216+ echo " Deploy fingerprint (${label} ): ${fingerprint} "
217+ }
218+
219+ count_files_under () {
220+ local root=" $1 "
221+ find " ${root} " -type f 2> /dev/null | wc -l | tr -d ' '
222+ }
223+
224+ count_files_in_web_container () {
225+ local container_dir=" $1 "
226+ docker compose exec -T web bash -lc " find '${container_dir} ' -type f 2>/dev/null | wc -l | tr -d ' '"
227+ }
228+
229+ count_files_in_proxy_container () {
230+ local container_dir=" $1 "
231+ docker compose exec -T proxy sh -lc " find '${container_dir} ' -type f 2>/dev/null | wc -l | tr -d ' '"
232+ }
233+
182234reset_container_dir () {
183235 local cid=" $1 "
184236 local target_dir=" $2 "
@@ -195,19 +247,13 @@ copy_tree_via_compose_cp() {
195247 docker compose cp " ${STATIC_FRONTEND} /." " web:${dest} /"
196248}
197249
198- copy_tree_via_podman_cp () {
199- local cid=" $1 "
200- local dest=" $2 "
201- podman cp " ${STATIC_FRONTEND} /." " ${cid} :${dest} /"
202- }
203-
204250copy_tree_via_podman_exec_tar () {
205251 local cid=" $1 "
206252 local dest=" $2 "
207253 tar -C " ${STATIC_FRONTEND} " -cf - . | podman exec -i " ${cid} " tar -xf - -C " ${dest} "
208254}
209255
210- copy_frontend_tree_into_web () {
256+ copy_tree_into_container () {
211257 local cid=" $1 "
212258 local dest=" $2 "
213259
@@ -227,15 +273,9 @@ copy_frontend_tree_into_web() {
227273 exit 1
228274 fi
229275
230- echo " rebuild_frontend.sh: compose cp unavailable; resetting ${dest} " >&2
231- reset_container_dir " ${cid} " " ${dest} "
232-
233- if copy_tree_via_podman_cp " ${cid} " " ${dest} " ; then
234- echo " rebuild_frontend.sh: copied frontend tree via podman cp → ${dest} " >&2
235- return
236- fi
237-
238- echo " rebuild_frontend.sh: podman cp failed; trying podman exec -i tar pipe → ${dest} " >&2
276+ # podman cp can write to the container layer instead of a bind-mounted volume on some
277+ # podman-compose setups; streaming tar through podman exec -i targets the mount reliably.
278+ echo " rebuild_frontend.sh: copying via podman exec -i tar → ${dest} " >&2
239279 reset_container_dir " ${cid} " " ${dest} "
240280 copy_tree_via_podman_exec_tar " ${cid} " " ${dest} "
241281}
@@ -247,11 +287,23 @@ copy_frontend_into_web() {
247287
248288 # nginx (proxy) reads STATIC_ROOT/frontend from the shared staticfiles_data volume.
249289 reset_container_dir " ${cid} " " ${CONTAINER_STATIC_ROOT_FRONTEND} "
250- copy_frontend_tree_into_web " ${cid} " " ${CONTAINER_STATIC_ROOT_FRONTEND} "
290+ copy_tree_into_container " ${cid} " " ${CONTAINER_STATIC_ROOT_FRONTEND} "
251291
252292 # Keep image source tree in sync for future collectstatic / container rebuilds.
253293 reset_container_dir " ${cid} " " ${CONTAINER_STATIC_FRONTEND} "
254- copy_frontend_tree_into_web " ${cid} " " ${CONTAINER_STATIC_FRONTEND} "
294+ copy_tree_into_container " ${cid} " " ${CONTAINER_STATIC_FRONTEND} "
295+ }
296+
297+ sha256_in_web_container () {
298+ local container_path=" $1 "
299+ docker compose exec -T web bash -lc \
300+ " if [[ ! -f '${container_path} ' ]]; then exit 2; fi; sha256sum '${container_path} ' | awk '{print \$ 1}'"
301+ }
302+
303+ sha256_in_proxy_container () {
304+ local container_path=" $1 "
305+ docker compose exec -T proxy sh -lc \
306+ " if [[ ! -f '${container_path} ' ]]; then exit 2; fi; sha256sum '${container_path} ' | awk '{print \$ 1}'"
255307}
256308
257309verify_container_path_matches_host () {
@@ -262,10 +314,7 @@ verify_container_path_matches_host() {
262314 local host_sha container_sha
263315
264316 host_sha=" $( file_sha256 " ${host_probe} " ) "
265- container_sha=" $(
266- docker compose exec -T web bash -lc \
267- " if [[ ! -f '${container_probe} ' ]]; then exit 2; fi; sha256sum '${container_probe} ' | awk '{print \$ 1}'"
268- ) "
317+ container_sha=" $( sha256_in_web_container " ${container_probe} " ) "
269318
270319 if [[ -z " ${container_sha} " ]]; then
271320 echo " rebuild_frontend.sh: ${label} probe missing after copy: ${container_probe} " >&2
@@ -282,6 +331,59 @@ verify_container_path_matches_host() {
282331 echo " Verified ${label} matches host (${container_probe} )"
283332}
284333
334+ verify_container_file_count_matches_host () {
335+ local container_dir=" $1 "
336+ local label=" $2 "
337+ local host_count container_count
338+
339+ host_count=" $( count_files_under " ${STATIC_FRONTEND} " ) "
340+ container_count=" $( count_files_in_web_container " ${container_dir} " ) "
341+
342+ if [[ " ${host_count} " != " ${container_count} " ]]; then
343+ echo " rebuild_frontend.sh: ${label} file count mismatch (host ${host_count} , web ${container_count} )" >&2
344+ return 1
345+ fi
346+
347+ echo " Verified ${label} file count matches host (${host_count} files)"
348+ }
349+
350+ verify_proxy_frontend_matches_web () {
351+ local web_probe=" ${CONTAINER_STATIC_ROOT_FRONTEND} /machine/index.html"
352+ local proxy_probe=" ${PROXY_STATIC_ROOT_FRONTEND} /machine/index.html"
353+ local web_sha proxy_sha
354+
355+ web_sha=" $( sha256_in_web_container " ${web_probe} " ) "
356+ proxy_sha=" $( sha256_in_proxy_container " ${proxy_probe} " ) "
357+
358+ if [[ -z " ${proxy_sha} " ]]; then
359+ echo " rebuild_frontend.sh: proxy nginx volume missing ${proxy_probe} " >&2
360+ return 1
361+ fi
362+
363+ if [[ " ${web_sha} " != " ${proxy_sha} " ]]; then
364+ echo " rebuild_frontend.sh: proxy nginx volume does not match web STATIC_ROOT" >&2
365+ echo " web: ${web_sha} (${web_probe} )" >&2
366+ echo " proxy: ${proxy_sha} (${proxy_probe} )" >&2
367+ return 1
368+ fi
369+
370+ echo " Verified proxy nginx volume matches web (${proxy_probe} )"
371+ }
372+
373+ verify_proxy_file_count_matches_web () {
374+ local web_count proxy_count
375+
376+ web_count=" $( count_files_in_web_container " ${CONTAINER_STATIC_ROOT_FRONTEND} " ) "
377+ proxy_count=" $( count_files_in_proxy_container " ${PROXY_STATIC_ROOT_FRONTEND} " ) "
378+
379+ if [[ " ${web_count} " != " ${proxy_count} " ]]; then
380+ echo " rebuild_frontend.sh: proxy file count mismatch (web ${web_count} , proxy ${proxy_count} )" >&2
381+ return 1
382+ fi
383+
384+ echo " Verified proxy nginx volume file count matches web (${web_count} files)"
385+ }
386+
285387verify_container_frontend_matches_host () {
286388 verify_container_path_matches_host " ${CONTAINER_STATIC_ROOT_FRONTEND} " " STATIC_ROOT/frontend (nginx volume)"
287389}
@@ -297,10 +399,15 @@ deploy_to_compose() {
297399 echo " Copying built assets into web:${CONTAINER_STATIC_ROOT_FRONTEND} (nginx staticfiles volume) ..."
298400 copy_frontend_into_web
299401 verify_container_frontend_matches_host
402+ verify_container_file_count_matches_host " ${CONTAINER_STATIC_ROOT_FRONTEND} " " STATIC_ROOT/frontend (nginx volume)"
300403 verify_spa_shells_via_compose " ${CONTAINER_STATIC_ROOT_FRONTEND} " " STATIC_ROOT/frontend"
404+ verify_proxy_frontend_matches_web
405+ verify_proxy_file_count_matches_web
406+ print_container_deploy_fingerprint " web STATIC_ROOT" web " ${CONTAINER_STATIC_ROOT_FRONTEND} /machine/index.html"
407+ print_container_deploy_fingerprint " proxy nginx volume" proxy " ${PROXY_STATIC_ROOT_FRONTEND} /machine/index.html"
301408
302- echo " Frontend deploy complete. nginx serves updated /static/ and /machine/ assets ."
303- echo " Pipeline was not restarted. Hard-refresh the browser to load new bundle hashes ."
409+ echo " Frontend deploy complete. Use the proxy service (ports 80/443), not web:8000 — Gunicorn does not serve /machine/."
410+ echo " Hard-refresh the browser and confirm the deploy fingerprint above matches page-*.js in DevTools Network ."
304411}
305412
306413main () {
0 commit comments