# ─────────────────────────────────────────────────────────────────────────────
# Kaidera OS Harness Console - HEADLESS container image (Milestone 1 T13).
#
# WHY THIS EXISTS: Milestone 1 moved live run-state out of process memory into the
# durable app-DB (run_state / run_span). With state no longer in the process, the
# console backend can be RESTARTED mid-run without losing what every agent is
# doing - so it becomes a proper headless Docker service in the `kaidera-os-cortex`
# stack, not a Mac app. This is the containerization that whole move enables.
#
# SAME ASGI APP, NO CODE BRANCHES: `app/main.py` is shell-agnostic; harness CLIs
# are invoked via subprocess, never imported. The exact same `app.main:app` runs
# here under plain uvicorn and in the native managed-service deployment.
#
# RUNTIME DEPS ONLY: requirements.txt contains only application dependencies.
# Development fitness tooling lives in requirements-fitness.txt and is never
# installed in this image. uvicorn serves HTTP and SSE.
#
# ⚠️ WORKER-SPAWN BOUNDARY (the honest M1 limit): the LLM harness CLIs
# (`claude-code` / `pi`) and their login live on the HOST Mac, not in this slim
# image. So this container serves the backend's HTTP / SSE / store / orchestrator
# / watchdog surface, but autonomous agent *spawning* is NOT container-resident
# yet — it stays host-side until the platform-later remote harness-service. The
# state move does NOT require the spawn to move: the detached worker writes the
# app-DB directly whether it runs on host or in-container, so a restart of THIS
# container never loses run-state. Do not try to bake the harness CLIs in here.
#
# SPA (Track C): the refined React/Vite console is built in a SEPARATE node stage
# (below) and only its static `dist/` bundle is COPYed into the slim python runtime
# — so node + node_modules never reach the final image. The runtime serves that
# bundle at /app (StaticFiles mount in app/main.py); the legacy HTML console at /
# is unchanged.
#
# Local machine only. NOT the Kaidera AI platform deployment, NOT a customer deploy.
# Build context is the repository root because the SPA embeds canonical help files
# from docs/help. Dockerfile.dockerignore limits that context to the console and
# those help files.
# ─────────────────────────────────────────────────────────────────────────────

# ── Stage 1: build the SPA (Track C) ─────────────────────────────────────────
# node-only build stage: install the locked deps + run the production Vite build
# (`npm run build` = tsc -b + vite build, base '/app/'), producing spa/dist. This
# stage is DISCARDED — only its dist/ output is copied forward, so the runtime image
# carries no node, no node_modules, no SPA source. Pinned to an LTS node on the same
# Debian (bookworm) base family as python:3.12-slim for a consistent toolchain.
FROM node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3 AS spa-build
WORKDIR /workspace/local-cortex/console/spa
# Deps first (layer-cached until the lockfile changes). npm's macOS-generated
# lock omits two Linux transitive entries. Refresh lock metadata with scripts
# disabled, then require the exact reviewed Linux lock before installing it.
COPY local-cortex/console/spa/package.json local-cortex/console/spa/package-lock.json ./
ARG SPA_LINUX_LOCK_SHA256=2386a505a5126d3e664cdded206775ab8dcc96e34600872547b413bb67e2ef84
RUN npm install --package-lock-only --ignore-scripts --no-audit --no-fund \
    && printf '%s  package-lock.json\n' "$SPA_LINUX_LOCK_SHA256" | sha256sum -c -
RUN npm ci --no-audit --no-fund
# Preserve the repository-relative path used by HelpContent.ts, then include only
# the canonical help payload it imports. node_modules and dist are excluded by
# Dockerfile.dockerignore.
COPY local-cortex/console/spa/ ./
COPY docs/help/ /workspace/docs/help/
RUN npm run build

# ── Stage 2: the headless python runtime ─────────────────────────────────────
FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de

# Fail fast, no .pyc clutter, unbuffered logs (so `docker logs` is live).
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

WORKDIR /app

# Dependencies first (layer-cached until requirements.txt changes). The runtime
# manifest deliberately excludes all build and fitness tooling.
COPY local-cortex/console/requirements.txt ./
RUN pip install -r requirements.txt

# The ASGI app. Only the package is needed at runtime.
COPY local-cortex/console/app ./app

# The built SPA bundle (Track C) from the node stage → /app/spa/dist, the exact
# path app/main.py serves at /app (SPA_DIST_DIR = <pkg-parent>/spa/dist; with the
# app package at /app/app, its parent is /app, so the bundle is /app/spa/dist). The
# mount is guarded — a missing bundle is logged + skipped — but this COPY ensures it
# is present, so the deployed container serves the SPA at /app. Only the static
# output is copied; no node/node_modules enter the runtime image.
COPY --from=spa-build /workspace/local-cortex/console/spa/dist ./spa/dist

# The console's HTTP/SSE port (matches the dev port + the compose publish).
EXPOSE 8765

# Single uvicorn worker — the M1 win is RESTARTABILITY, not horizontal scale
# (single-user local console). No code branch: the same app object as every other
# shape. host 0.0.0.0 so the published 127.0.0.1:8765 mapping reaches it.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8765"]
