# althing v3 — the post office. The fleet's message bus.
#
# MOVED nh3-dev -> nh3-docker on 2026-08-28 (operator: "I want it on the docker
# machine — that was always the goal"). It was deployed to nh3-dev at the U9b flag
# day because the herald lives there; but the herald is the piece that MUST be
# host-local (it reads route files and pokes FIFOs), and the post office is
# explicitly the piece that is not.
#
# Three reasons the dev box was wrong:
#   - our own server table calls nh3-dev "not a Docker-stack host", and NH-site
#     non-GPU services belong here.
#   - nh3-dev had three OOM events in fourteen days with the interval HALVING
#     (2026-08-14, -26, -28), and the confirmed hog is Claude Code sessions at
#     5-18 GB each, which is that box's actual job.
#   - `mem_limit` protects the fleet FROM the post office. It does nothing to
#     protect the post office from the box: oom_score_adj is 0, so it was an
#     ordinary kill candidate, and the 08-28 sweep took althing-herald and
#     uvicorn. A sweep that takes the post office takes mail for all 73 handles.
#
# ⚠ MOVING THE DATA: `docker stop` does NOT checkpoint the WAL. Measured on the
# 08-28 move: post_office.db was 155 KB / mtime 15:09 while post_office.db-wal
# was 4.1 MB / mtime 16:56 — every recent message lived in the WAL. Copying the
# .db alone yields a database that opens cleanly, passes a smoke test, and is
# missing the day's mail. Stop the container, then
# `PRAGMA wal_checkpoint(TRUNCATE)` explicitly, then verify row counts on BOTH
# sides before deleting anything.
#
# Deploy:  scripts/deploy-stack.sh nh3-docker althing-post-office
# Image:   built from the althing repo's Dockerfile (vh/althing @ v3.0.0), pushed to
#          the gitea registry 2026-08-28. Rebuild + republish:
#
#            cd ~/development/althing
#            docker build -t gitea.phasefinal.com/claude-bot/althing-post-office:<ver> .
#            echo $(cat ~/.config/claude-bot/gitea-token) | \
#              docker login gitea.phasefinal.com -u claude-bot --password-stdin
#            docker push gitea.phasefinal.com/claude-bot/althing-post-office:<ver>
#
#          then update the digest below and redeploy. Supersedes the
#          `docker save | ssh | docker load` hand-carry the move originally used.
#
# ⚠ NAMESPACE IS `claude-bot`, NOT `vh`. claude-bot's token carries write:package
#   but package namespaces are owned: pushing to `vh/...` returns
#   `unauthorized: authentication required` AFTER a successful `docker login`, which
#   reads like a credential fault and is actually an ownership one. Publishing under
#   claude-bot's own namespace also satisfies the standing directive to stop reusing
#   the operator's personal credentials for infra work. Both hosts are logged in as
#   claude-bot; a new host needs that login before it can pull.

services:
  post-office:
    # Digest-pinned, not tag-floating: `:3.0.0` is a mutable pointer on a registry
    # anyone can re-push, and this container is the fleet's whole message bus. The
    # tag is kept alongside the digest purely so a human can read what it is.
    image: gitea.phasefinal.com/claude-bot/althing-post-office:3.0.0@sha256:410fed41fa049c41cf83577fd2e49831ea1959ca50b597a8bd35a548e267cf01
    container_name: althing-post-office

    # ─── Host networking, so the bind guard keeps working ────────────
    #
    # ⚠ DO NOT "fix" this into a bridged container with `-p`. api.py's
    # resolve_bind_host refuses any address that resolves to a wildcard, and it
    # tests the RESOLVED PROPERTY rather than matching strings, so there is no
    # spelling of "everything" that gets past it. A bridged container cannot
    # satisfy that guard honestly: inside its own netns the only reachable bind
    # is a wildcard, and publishing the port would move access control from the
    # address the application checks to a `-p` flag it cannot see.
    #
    # Reachability on the private network IS the authorisation story here —
    # there is no login and none is wanted.
    #
    # Cost, stated plainly: no network namespace isolation, and port 8390 is
    # claimed host-wide. For a single-service private-network deployment that is
    # the right trade, but it IS a trade.
    network_mode: host

    # The store is the only thing that must survive. Named volume rather than a
    # bind mount: uid 1000 inside the container owns it, and docker creates it
    # with the right ownership instead of inheriting the host path's.
    volumes:
      - post-office-data:/var/lib/althing

    # The private address of THIS box. Single place it is named; the image ships
    # no default on purpose, so a deployment that omits it is refused at startup
    # rather than binding wide.
    environment:
      ALTHING_BIND_HOST: "10.100.50.40"
      ALTHING_PORT: "8390"
      ALTHING_DB: "/var/lib/althing/post_office.db"

    # `unless-stopped` rather than `always` so an operator who deliberately stops
    # it during a flag day does not find it running again after a reboot.
    restart: unless-stopped

    # One Python interpreter holding one SQLite connection; it idles far below
    # this. The cap is not a tuning parameter, it is a promise that the post
    # office can never be its host's next OOM story.
    #
    # ⚠ nh3-docker runs Compose v5, which ignores `version:` and honours
    # `mem_limit` directly. On a docker-compose 1.x host this needs schema 2.4 —
    # under 3.x the key moves to `deploy:`, which is swarm-only and SILENTLY
    # IGNORED. Verify with `docker inspect` (want 536870912), never by reading
    # the yaml: a cap that does nothing reads as protection.
    mem_limit: 512m

    # ⚠ The fleet's entire bus. Make the kernel shoot almost anything else first.
    # This is the gap the nh3-dev deployment had: a 512m cap and oom_score_adj 0
    # means "cannot cause an OOM, is an ordinary victim of one".
    oom_score_adj: -500

    # Docker's default json-file driver has no size limit. v2's herald left a
    # 60 MB log on nh3-dev; an uncapped container log is the same mistake with a
    # different name.
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

    # Inherited from the image, restated so it is visible at deploy time rather
    # than only in `docker inspect`.
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://10.100.50.40:8390/',timeout=4).status==200 else 1)"
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

    # `Toolchain` is an EXISTING group under the existing Toolchain tab in
    # stacks/homepage/conf/settings.yaml — "the plumbing", which is where a
    # message bus belongs. Naming a group the layout has never heard of gets no
    # `tab:` and renders the group on ALL FOUR tabs (the Scriberr "AI Systems"
    # bug, 2026-08-23), so this must stay a group that already exists.
    #
    # ⚠ Labels bind at container CREATION. Editing them needs `up -d`, never
    # `restart` — a restart leaves the old labels in place and the dashboard
    # keeps showing whatever was there before.
    #
    # nh3-docker is a discovered Docker host in homepage's docker.yaml (as
    # `nh3-pfi-docker`), so the label is enough; do NOT also add a services.yaml
    # entry or the card renders twice.
    labels:
      - homepage.group=Toolchain
      - homepage.name=althing post office
      - homepage.icon=mdi-mailbox
      - homepage.description=althing v3 message bus — handles, unread counts, node liveness
      - homepage.href=http://10.100.50.40:8390

volumes:
  post-office-data:
    name: althing-post-office-data
