From 039d9b950718e69894146c42e40cd81301fd99f3 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 11 May 2026 13:18:16 -0700 Subject: [PATCH] C1: pull shower sdist for vendored static (fixes /host/ 500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wheel ships config/ and shower/ only (per pyproject hatchling config), leaving the repo's top-level static/ dir — Sortable.min.js, cropper.min.js, cropper.min.css, prize-placeholder.svg — behind. At runtime, host_dashboard.html's {% static 'css/cropper.min.css' %} hits the manifest, CompressedManifestStaticFilesStorage raises ValueError on the missing entry, /host/ returns 500. Fix on the deploy side: fetch the sdist via fetchurl (pinned SRI hash from forge PyPI), extract its top-level static/ subtree into a non-FOD derivation, lay it down at /app/static in the image. The local_settings shim adds /app/static to STATICFILES_DIRS so collectstatic at boot picks the vendored assets up alongside the Django admin's own static files. Sdist URL is forge.ops.eblu.me/api/packages/... (tailnet) — matches the just-landed edge block on forge.eblu.me/api/packages/*. The nix-container-builder runner on ringtail is on the tailnet, so the FOD fetch works. App doesn't change. v1.0.3 is no longer needed for the static gap — the wheel's "packages = [config, shower]" pattern stays as-is, and we treat the sdist as the canonical bundle for the assets the wheel intentionally omits. Co-Authored-By: Claude Opus 4.7 (1M context) --- containers/shower/default.nix | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/containers/shower/default.nix b/containers/shower/default.nix index 1b12649..d9863e1 100644 --- a/containers/shower/default.nix +++ b/containers/shower/default.nix @@ -25,6 +25,28 @@ let python = pkgs.python314; + # The repo's top-level static/ directory (vendored Sortable + cropper + # JS/CSS, prize placeholder SVG) isn't shipped in the wheel — hatchling + # only packages config/ and shower/, leaving the repo-root static/ + # behind. Pull the sdist (which contains the full source tree) and + # extract just the static/ subtree into the image as /app/static. + # local_settings adds it to STATICFILES_DIRS so collectstatic at boot + # picks it up alongside the Django admin's static files. + # + # Fetched from forge.ops.eblu.me (tailnet) because /api/packages/* is + # blocked at the fly edge — see fly/nginx.conf forge.eblu.me block. + # Hash is the upstream sha256 from forge PyPI's simple index. + showerSdist = pkgs.fetchurl { + name = "adelaide_baby_shower_app-${version}.tar.gz"; + url = "https://forge.ops.eblu.me/api/packages/eblume/pypi/files/adelaide-baby-shower-app/${version}/adelaide_baby_shower_app-${version}.tar.gz"; + hash = "sha256-nlCtlx9zuYaLoJZSckybLV5YPpA8vZamN96O3RXOstM="; + }; + + staticAssets = pkgs.runCommand "shower-static-assets-${version}" { } '' + ${pkgs.gnutar}/bin/tar -xzf ${showerSdist} -C $TMPDIR + cp -r $TMPDIR/adelaide_baby_shower_app-${version}/static $out + ''; + # Fixed-output derivation: pip-installs the app wheel + every transitive # dep into a single target dir. FODs get network access in exchange for # a pinned output hash, which means the whole dependency closure is @@ -147,11 +169,17 @@ let # otherwise resolve to site-packages, scattering db.sqlite3 / media / # staticfiles into the venv. Pin them to /app/{data,media,data/staticfiles}. localSettings = pkgs.writeText "local_settings.py" '' + from pathlib import Path + from config.settings import * # noqa: F401,F403 DATABASES["default"]["NAME"] = "/app/data/db.sqlite3" MEDIA_ROOT = "/app/media" STATIC_ROOT = "/app/data/staticfiles" + # /app/static comes from the repo-root static/ subtree of the sdist + # (see default.nix staticAssets). Added because the wheel doesn't + # ship vendored Sortable/cropper assets. + STATICFILES_DIRS = [Path("/app/static")] ''; # PYTHONPATH, DJANGO_SETTINGS_MODULE, PATH, and HOME live in the image's @@ -195,6 +223,8 @@ pkgs.dockerTools.buildLayeredImage { mkdir -p app/data app/media tmp chmod 1777 tmp cp ${localSettings} app/local_settings.py + cp -r ${staticAssets} app/static + chmod -R u+w app/static ''; fakeRootCommands = ''