The public site (nextcraft.coreci.dev) 404s because HAProxy forwards the
domain to the ai-service alone and the web app was never reachable (only
:8420 is open). This makes ONE port serve BOTH surfaces:
- Web: `output: 'export'` static build (apps/web/out) with generateStaticParams
for all 7 dynamic routes; NEXT_PUBLIC_AI_SERVICE_URL=self bakes relative
same-origin fetches (engine-base-url "" base; engine-client already
concatenates). No CORS, no mixed content, no Node web server in prod.
- API: optional AI_WEB_STATIC_DIR StaticFiles(html=True) mount AFTER all
routers (API wins; / serves index.html; unknown paths serve the export's
404.html with status 404; set-but-missing dir fails startup loudly).
Default unset — dev/tests unchanged.
- CLI: `dev -d/--detach` (daemon; ~/.nextcraft/run/<clone-hash>/dev.{pid,log},
refuses double-start, auto-wires AI_WEB_STATIC_DIR when out/ exists),
`stop` (SIGTERM→SIGKILL 5s), `log` (-n N lines, -f follow). stdlib only.
Founder directives (this hotfix):
- unattended/daemonized dev with a tail command — dev -d / log / stop
- only 8420 reachable — single-port same-origin topology
- runtime state must NOT live in the repo — db_path default moves to
~/.nextcraft/data/nextcraft.db, sandbox_dir to ~/.nextcraft/sandboxes/;
expanduser validator (AI_DB_PATH=~/... works); .env.example stops pinning
repo paths; daemon pid/log also under ~/.nextcraft.
Test hygiene: conftest settings fixture pins stores to tmp_path (was silently
using repo defaults); sandbox tests use a bind-mount-safe sandbox_dir fixture
(overlayfs /tmp breaks userns bind mounts with ENODEV — home ext4 works).
Gates: typecheck 8/8; pnpm build emits out/ (all 73 pages static); web tests
5/5; cli tests green incl. real daemon lifecycle; backend 413 passed
(+4 static-mount tests); ruff clean.
---ci---
phase: hotfix
milestone: v0.4
status: complete
type: hotfix
---/ci---
Task 2-3-01: create(learner_id, task_id) — telemetry sandboxes run a persistent
helper/inner namespace topology (offline inner ns; agent joins mount ns only and stays
online to reach the loopback ingest). Capture agent copied into the workspace (visible
in-ns at the bind), launched via sh -c with in-ns absolute paths (host cwd invalid after
the nsenter mount swap), stdin=DEVNULL daemonizes the agent (lifecycle tied to sandbox:
destroy reaps agent -> inner -> helper). Wire contract: frames strip URL-owned identity
(ingest extra=forbid anti-spoofing); spool keeps full events.
E2E test (real uvicorn on ephemeral port): exec in a live namespace sandbox -> events
arrive at WS ingest -> SQLite, ordered, sandbox-scoped. Pure-shell path (task_id=None)
asserts no capture agent. Full suite 216 green; ruff clean.
---ci---
phase: 2
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-003], partial: []}
---/ci---