task-board: add stack + elway deploy playbook
stacks/task-board/compose.yaml + .env.example describe the runtime —
image tag is task-board:local (built on the host), 7878 on host maps
to 7878 in container, SQLite lives at /opt/docker/conf/task-board/data/
(bind mount, uid 1000 friendly), homepage auto-card labels under
Toolchain group, on traefik-net like the rest of the fleet.
playbooks/deploy-task-board.yaml is the first real elway playbook —
exercises everything we built tier-1 + tier-2 idempotency for:
- `creates:` on mkdir + first-time clone + compose dir + data dir
- `when:` to chown /opt/docker/build only if it came up root-owned
- `when:` to seed .env only if one doesn't already exist (never
clobbers user edits on rerun)
- `changed_when:` on the `git reset --hard` step so repeat runs
against the same ref report `ok` instead of `changed`
- `changed_when: "false"` on every verify step (they attest, not
change)
- `upload:` with mode for compose.yaml + .env
Post-up the playbook polls /api/health for 30s before handing off to
the verify phase, so verification doesn't race the healthcheck's
start_period. Verify covers: /api/health 200, /api/tasks shape, /mcp
reachable, container on traefik-net.
Prereqs documented in the playbook header: Docker + compose plugin,
traefik-net network, git SSH access to gitea from the target host.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# Deploy task-board (https://gitea.phasefinal.com/vh/task-board) to a
|
||||
# Docker host following the PFI /opt/docker/ convention (ana-docker by
|
||||
# default, but the playbook works against any host with Docker +
|
||||
# traefik-net in place).
|
||||
#
|
||||
# Idempotent: rerunning is safe. Creates-gates + conditional when:
|
||||
# checks skip work that's already done; `docker compose up -d` is itself
|
||||
# idempotent (no restart unless compose content or env changed).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/elway ana-docker --playbook playbooks/deploy-task-board.yaml
|
||||
# scripts/elway ana-docker --playbook playbooks/deploy-task-board.yaml --var ref=v0.1.0
|
||||
#
|
||||
# Prereqs on the target host:
|
||||
# - Docker + docker compose plugin
|
||||
# - `traefik-net` docker network (external)
|
||||
# - Target user (lkraven) has git SSH access to gitea.phasefinal.com
|
||||
# — either SSH key authorized in gitea, or the repo is HTTPS-reachable
|
||||
# if you swap `repo_url` below.
|
||||
# - Target user is in the `docker` group.
|
||||
|
||||
vars:
|
||||
repo_url: git@gitea.phasefinal.com:vh/task-board.git
|
||||
ref: main
|
||||
build_dir: /opt/docker/build/task-board
|
||||
image_tag: task-board:local
|
||||
compose_dir: /opt/docker/compose/task-board
|
||||
data_dir: /opt/docker/conf/task-board/data
|
||||
host_port: "7878"
|
||||
|
||||
steps:
|
||||
# ── host-side directory prep ─────────────────────────────────────────
|
||||
- name: Ensure /opt/docker/build parent exists
|
||||
shell: mkdir -p /opt/docker/build
|
||||
sudo: true
|
||||
creates: /opt/docker/build
|
||||
|
||||
- name: Chown /opt/docker/build to lkraven (only if mkdir'd by root above)
|
||||
shell: chown lkraven:lkraven /opt/docker/build
|
||||
sudo: true
|
||||
when: '[ "$(stat -c %U /opt/docker/build)" != lkraven ]'
|
||||
|
||||
# ── fetch / sync source ─────────────────────────────────────────────
|
||||
- name: Clone task-board repo if absent
|
||||
# Auto-accept the first-run host key so the playbook doesn't hang
|
||||
# prompting for yes/no.
|
||||
shell: GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new" git clone {{ repo_url }} {{ build_dir }}
|
||||
creates: "{{ build_dir }}/.git"
|
||||
|
||||
- name: Fetch from origin
|
||||
shell: cd {{ build_dir }} && git fetch --quiet origin
|
||||
|
||||
- name: Reset working tree to origin/{{ ref }}
|
||||
shell: cd {{ build_dir }} && git reset --hard origin/{{ ref }}
|
||||
# Report ok (no-change) when the tree was already at the requested
|
||||
# ref — saves a noisy CHANGED status line on no-op reruns.
|
||||
changed_when: '[ "$(cd {{ build_dir }} && git rev-parse HEAD 2>/dev/null)" != "$(cd {{ build_dir }} && git rev-parse origin/{{ ref }} 2>/dev/null)" ]'
|
||||
|
||||
# ── image build ─────────────────────────────────────────────────────
|
||||
- name: Build image {{ image_tag }}
|
||||
shell: cd {{ build_dir }} && docker build -t {{ image_tag }} .
|
||||
# Docker build reuses layer cache and is fast on reruns, but it
|
||||
# always runs — we can't cheaply know up-front whether anything
|
||||
# downstream has changed. Leave it in the always-run lane; Docker
|
||||
# itself handles the no-op efficiently.
|
||||
|
||||
# ── compose + data dirs ─────────────────────────────────────────────
|
||||
- name: Ensure compose dir exists
|
||||
shell: mkdir -p {{ compose_dir }}
|
||||
creates: "{{ compose_dir }}"
|
||||
|
||||
- name: Ensure data dir exists
|
||||
# Created as lkraven (uid 1000 on these hosts), matching the
|
||||
# container's `app` user — no chown dance needed.
|
||||
shell: mkdir -p {{ data_dir }}
|
||||
creates: "{{ data_dir }}"
|
||||
|
||||
# ── deploy compose files ────────────────────────────────────────────
|
||||
- name: Upload compose.yaml
|
||||
upload:
|
||||
src: stacks/task-board/compose.yaml
|
||||
dest: "{{ compose_dir }}/compose.yaml"
|
||||
mode: "0644"
|
||||
|
||||
- name: Seed .env from template (only if absent)
|
||||
upload:
|
||||
src: stacks/task-board/.env.example
|
||||
dest: "{{ compose_dir }}/.env"
|
||||
mode: "0644"
|
||||
when: "[ ! -f {{ compose_dir }}/.env ]"
|
||||
|
||||
# ── bring up + wait for ready ───────────────────────────────────────
|
||||
- name: docker compose up -d
|
||||
shell: cd {{ compose_dir }} && docker compose up -d
|
||||
|
||||
- name: Wait for /api/health to respond
|
||||
# Short retry loop — docker compose up returns before healthcheck
|
||||
# stabilizes; we want verify: to run against a live server.
|
||||
shell: |
|
||||
for i in $(seq 1 30); do
|
||||
curl -sf -o /dev/null http://localhost:{{ host_port }}/api/health && exit 0
|
||||
sleep 1
|
||||
done
|
||||
exit 1
|
||||
changed_when: "false"
|
||||
|
||||
verify:
|
||||
- name: /api/health returns 200
|
||||
shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/api/health
|
||||
changed_when: "false"
|
||||
|
||||
- name: /api/tasks returns a JSON tasks list
|
||||
shell: curl -sf http://localhost:{{ host_port }}/api/tasks | grep -q '"tasks"'
|
||||
changed_when: "false"
|
||||
|
||||
- name: MCP endpoint reachable (expect 3xx redirect for GET /mcp → /mcp/)
|
||||
shell: '[ "$(curl -s -o /dev/null -w %{http_code} http://localhost:{{ host_port }}/mcp)" -ge 300 ]'
|
||||
changed_when: "false"
|
||||
|
||||
- name: Container is in the traefik-net network
|
||||
# `traefik-net` has a dash, so it's not accessible via Go template dot
|
||||
# syntax — JSON-encode the networks map and grep for the key instead.
|
||||
shell: docker inspect task-board --format '{{json .NetworkSettings.Networks}}' | grep -q traefik-net
|
||||
changed_when: "false"
|
||||
@@ -0,0 +1,25 @@
|
||||
# task-board stack tunables. Copy to `.env` on ana-docker before deploying.
|
||||
#
|
||||
# The deploy playbook seeds `.env` from this template on first run only —
|
||||
# it won't clobber an existing `.env`.
|
||||
|
||||
# Image tag. Built locally from the task-board git repo by the playbook.
|
||||
TASK_BOARD_IMAGE=task-board:local
|
||||
|
||||
# Host port exposing MCP + web UI (container listens on 7878 internally).
|
||||
TASK_BOARD_PORT=7878
|
||||
|
||||
# Bind address. 0.0.0.0 exposes to LAN; 127.0.0.1 restricts to localhost.
|
||||
# Claude Code running on another machine needs LAN (or SSH-forward + 127.0.0.1).
|
||||
TASK_BOARD_BIND=0.0.0.0
|
||||
|
||||
# Host path for the SQLite store. Container runs as uid 1000 — path must be
|
||||
# writable by that uid (mkdir'd by the playbook without sudo, so lkraven-owned
|
||||
# when lkraven is uid 1000 on the host).
|
||||
TASK_BOARD_DATA_DIR=/opt/docker/conf/task-board/data
|
||||
|
||||
# Seconds of inactivity before a card turns grey (dormant).
|
||||
TASK_BOARD_DORMANT_AFTER=3600
|
||||
|
||||
# Seconds archived (completed) cards are kept before hard-delete.
|
||||
TASK_BOARD_ARCHIVE_TTL=86400
|
||||
@@ -0,0 +1,51 @@
|
||||
# task-board
|
||||
|
||||
MCP + web dashboard that shows what the assistant is working on. Green
|
||||
(active) / Red (waiting) / Grey (dormant) cards with comment streams.
|
||||
|
||||
**Server:** ana-docker
|
||||
**Port:** 7878 (configurable via `.env`)
|
||||
**Upstream repo:** [vh/task-board](https://gitea.phasefinal.com/vh/task-board)
|
||||
**Image:** `task-board:local` — built on the host from the git repo by
|
||||
the deploy playbook. Not pulled from a registry.
|
||||
|
||||
## Deploy
|
||||
|
||||
Via elway — see `playbooks/deploy-task-board.yaml` in the eshpfi-management
|
||||
root. The playbook owns the full flow: clone/update the source repo,
|
||||
`docker build`, install compose + seed .env, bring up, verify health.
|
||||
|
||||
```bash
|
||||
# First deploy (or update to latest main)
|
||||
scripts/elway ana-docker --playbook playbooks/deploy-task-board.yaml
|
||||
|
||||
# Pin to a specific ref (tag, branch, or commit SHA)
|
||||
scripts/elway ana-docker --playbook playbooks/deploy-task-board.yaml --var ref=v0.1.0
|
||||
```
|
||||
|
||||
## Wiring into Claude Code
|
||||
|
||||
After deploy, add to `~/.claude/settings.json` (user-level) or `.mcp.json`
|
||||
(project-level):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-board": {
|
||||
"type": "http",
|
||||
"url": "http://10.250.50.70:7878/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Assistant gains four tools (`task_start`, `task_update`, `task_wait`,
|
||||
`task_complete`). Web UI at `http://10.250.50.70:7878/`.
|
||||
|
||||
## Path layout (on ana-docker)
|
||||
|
||||
| Host path | Container path | Purpose | Restic? |
|
||||
|---|---|---|---|
|
||||
| `/opt/docker/build/task-board/` | — | git checkout used as docker build context | excluded |
|
||||
| `/opt/docker/compose/task-board/` | — | compose.yaml + .env | included (via `/opt/docker`) |
|
||||
| `/opt/docker/conf/task-board/data/` | `/app/data` | SQLite (`tasks.db`) | **included** |
|
||||
@@ -0,0 +1,46 @@
|
||||
# task-board — MCP + web dashboard for assistant task state.
|
||||
#
|
||||
# Image is built on the host from the task-board git repo by the deploy
|
||||
# playbook (`playbooks/deploy-task-board.yaml`), which clones into
|
||||
# /opt/docker/build/task-board and runs `docker build -t task-board:local .`
|
||||
# before installing this compose and bringing it up.
|
||||
#
|
||||
# SQLite state persists under ${TASK_BOARD_DATA_DIR} (bind-mounted) so
|
||||
# container rebuilds don't lose task history.
|
||||
#
|
||||
# All tunables live in .env — edit that, not this file.
|
||||
|
||||
services:
|
||||
task-board:
|
||||
image: ${TASK_BOARD_IMAGE}
|
||||
container_name: task-board
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${TASK_BOARD_BIND:-0.0.0.0}:${TASK_BOARD_PORT}:7878"
|
||||
volumes:
|
||||
- ${TASK_BOARD_DATA_DIR}:/app/data
|
||||
environment:
|
||||
# Container always listens on 7878 internally; host port is the only knob.
|
||||
- TASK_BOARD_PORT=7878
|
||||
- TASK_BOARD_BIND=0.0.0.0
|
||||
- TASK_BOARD_DORMANT_AFTER=${TASK_BOARD_DORMANT_AFTER:-3600}
|
||||
- TASK_BOARD_ARCHIVE_TTL=${TASK_BOARD_ARCHIVE_TTL:-86400}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c 'import urllib.request,sys; r=urllib.request.urlopen(\"http://127.0.0.1:7878/api/health\",timeout=3); sys.exit(0 if r.status==200 else 1)' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- tnet
|
||||
labels:
|
||||
- homepage.group=Toolchain
|
||||
- homepage.name=task-board
|
||||
- homepage.icon=mdi-view-dashboard-variant
|
||||
- homepage.description=Assistant task state dashboard (MCP + web UI)
|
||||
- homepage.href=http://10.250.50.70:${TASK_BOARD_PORT}
|
||||
|
||||
networks:
|
||||
tnet:
|
||||
name: traefik-net
|
||||
external: true
|
||||
Reference in New Issue
Block a user