bytelyst-devops-tools/dashboard/backend/Dockerfile
Hermes VM eaaa545e6c feat(dashboard): close Phase 6 (trend cards + theme toggle), drop-root scaffold, Agents inventory, Phase 0 reconfirm
Closes the remaining tractable items from the carry-forward queue.

1. Drop-root scaffold for the backend container (P2 mitigation)
   `backend/Dockerfile` adds non-root `app` user (uid 1001) + `docker`
   group (gid via `DOCKER_GID` build arg, default 999). `BACKEND_USER`
   build arg defaults to `root` so existing deployments keep working;
   set it to `app` plus `DOCKER_GID=$(getent group docker | cut -d: -f3)`
   to flip the runtime non-root. `dashboard/DEPLOYMENT.md` gets a new
   "Running non-root" section with the exact `chgrp`/`chmod` recipe
   for the bind-mounted log files (the host-side prep that pairs with
   the build flip). DEPLOYMENT.md mitigation roadmap updated.

2. Phase 6 trend cards
   `lib/hermes-ops-history.ts` keeps the last 24 ops snapshots in
   localStorage (de-duped on `generatedAt`, schema-guarded on read,
   degrades silently on quota exceeded). Three trend cards in the
   ops panel:
     - Warning-volume sparkline + current count
     - Healthy-instance count sparkline (X/2)
     - Per-instance "minutes since last backup commit" with a 30m
       stale threshold
   SVG polyline sparklines, no chart library — `<svg viewBox="0 0
   100 100" preserveAspectRatio="none">` with `vector-effect:
   non-scaling-stroke` so the line stays 2px regardless of the
   parent's width.

3. Phase 6 theme toggle
   `components/theme-toggle.tsx` Sun/Moon button mounted in the
   Hermes layout next to the instance switcher. Persists in
   localStorage `bytelyst.theme.v1`. The design system already
   defined `[data-theme="light"]` overrides in `styles/tokens.css`;
   the toggle just sets the attribute. FOUC-prevention inline script
   in the root layout reads the same key BEFORE React hydrates so
   the first paint matches the user's last choice.

4. Phase 3 partial close: Agents pane → telemetry inventory
   `/hermes/agents` now renders a "Memory & Skills inventory (live)"
   SectionCard backed by the Phase 3 telemetry endpoint per instance
   — `hermes memory list` and `hermes skills list` rendered with
   per-section probe-status badges (`up`/`unknown`), item counts,
   and the first N entries each. Agent **health** statuses (latency,
   failure rate, last-success/failure) stay seed-data — observability
   for those needs a separate ingestion contract that the telemetry
   endpoint doesn't provide today.

5. Phase 0 reconfirmation
   Roadmap Phase 0 ticked with explicit verification notes for each
   guardrail (no public listener, manual approvals, secret hygiene,
   Caddy review). Remains "must hold throughout" — the ticks reflect
   today's verified state, not single-checkbox completion.

Verified: backend typecheck , 74/74 backend unit tests , web
typecheck , 7/7 E2E , lint 0 errors, build green, coverage gate
≥95% lines on every gated file.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-30 08:26:26 +00:00

81 lines
3.0 KiB
Docker

# Build context: bytelyst-devops-tools/dashboard/ (monorepo root)
# --- Stage 1: Build ---
FROM node:20-alpine AS builder
WORKDIR /app/backend
COPY backend/package.json backend/package-lock.json ./
RUN npm ci --ignore-scripts
COPY backend/tsconfig.json ./
COPY backend/src/ ./src/
# Build-time env vars (baked into the bundle)
ARG BYTELYST_COMMIT_SHA=unknown
ARG BYTELYST_COMMIT_SHA_FULL=unknown
ARG BYTELYST_BRANCH=unknown
ARG BYTELYST_BUILT_AT=unknown
ARG BYTELYST_COMMIT_AUTHOR=unknown
ARG BYTELYST_COMMIT_MESSAGE=unknown
ARG BYTELYST_DOCKER_IMAGE=devops-backend:latest
ENV BYTELYST_COMMIT_SHA=${BYTELYST_COMMIT_SHA} \
BYTELYST_COMMIT_SHA_FULL=${BYTELYST_COMMIT_SHA_FULL} \
BYTELYST_BRANCH=${BYTELYST_BRANCH} \
BYTELYST_BUILT_AT=${BYTELYST_BUILT_AT} \
BYTELYST_COMMIT_AUTHOR=${BYTELYST_COMMIT_AUTHOR} \
BYTELYST_COMMIT_MESSAGE=${BYTELYST_COMMIT_MESSAGE} \
BYTELYST_DOCKER_IMAGE=${BYTELYST_DOCKER_IMAGE}
RUN npm run build
# --- Stage 2: Run ---
# Use Debian slim (not Alpine) because vm-health-check.sh uses GNU df flags
# (--output=pcent, --output=avail) that BusyBox df does not support.
FROM node:20-slim AS runner
WORKDIR /app/backend
COPY backend/package.json backend/package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts
# Install tools needed by the VM management module:
# bash — vm-health-check.sh and vm-cleanup.sh require bash
# docker.io — docker CLI to communicate with the host daemon via socket
# python3 — used in inline python3 -c snippets inside the scripts
RUN apt-get update && apt-get install -y --no-install-recommends \
curl bash docker.io python3 \
&& rm -rf /var/lib/apt/lists/*
# Non-root user setup (Phase 5 P2 mitigation roadmap, item #4).
# The backend doesn't strictly need root — its only privileged action is
# talking to the docker daemon, which group membership covers. We create
# the user + a docker group at a build-arg-configurable GID so the GID
# can match the host's docker group (`getent group docker` on the host).
#
# Default `BACKEND_USER=root` keeps the current behaviour so existing
# deployments don't break. Set `BACKEND_USER=app` to run non-root; this
# requires the bind-mounted log files in `/var/log/vm-*.log` and
# `/var/log/docker-watchdog.log` to be group-readable+writable by the
# matching docker GID (or world-readable for read-only paths). See
# `dashboard/DEPLOYMENT.md` Privilege Surface → "Running non-root".
ARG BACKEND_USER=root
ARG DOCKER_GID=999
RUN groupadd --system --gid "${DOCKER_GID}" docker || true \
&& useradd --system --create-home --uid 1001 --gid "${DOCKER_GID}" --shell /sbin/nologin app \
&& chown -R app:"${DOCKER_GID}" /app
COPY --from=builder --chown=app:${DOCKER_GID} /app/backend/dist ./dist
ENV NODE_ENV=production
ENV PORT=4004
EXPOSE 4004
# Switch to non-root only when explicitly opted in via build arg. If the
# arg is `app`, the next two layers actually drop privileges; if `root`,
# they're a no-op.
USER ${BACKEND_USER}
CMD ["node", "dist/server.js"]