Operator ruling 2026-09-24. On ana-docker the stack is `docker compose down`: the container is removed and port 7878 is closed. Kept for revival: - the data dir /opt/docker/conf/task-board/data (tasks.db, last written 2026-09-11) - the task-board:local image - stacks/task-board/ and the host's compose + .env The Uptime Kuma monitor (id 3) was deleted before the stop so it could not page, and its row is removed from monitors.yaml. Homepage drops the card on its own, since it reads the container's labels. Hooks: the container log showed no hook POSTs in 30 days. The only traffic was open browser tabs holding /events, and the plugin was already uninstalled on nh3-dev. Removed the paragraph that told sessions to call task_* tools (CLAUDE.md, and the fork-fleet.sh template that seeds new repos), the vestigial TASK_BOARD_SESSION env in .claude/settings.json, and the listings in README and FLEETTOOLS.
315 lines
11 KiB
Bash
Executable File
315 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# fork-fleet.sh — bootstrap a fresh fleet-management repo from this one.
|
|
#
|
|
# Mirrors the reusable tooling (scripts/, generic playbook templates,
|
|
# conventions section of CLAUDE.md, .gitignore) into a new directory
|
|
# and strips everything fleet-specific (servers/, stacks/, configs/,
|
|
# fleet-named playbooks, runbooks, status). The output is an empty
|
|
# but conventionally-organized workspace that's ready to populate
|
|
# with a different fleet.
|
|
#
|
|
# Deliberately:
|
|
# * does NOT create a remote / push — that's a manual step so the
|
|
# name + namespace are explicit
|
|
# * does NOT carry git history — the new fleet starts with a clean
|
|
# "Initial commit" so the source repo's commit log doesn't bleed
|
|
# across
|
|
# * refuses to overwrite an existing directory
|
|
#
|
|
# Usage:
|
|
# scripts/fork-fleet.sh <destination-dir> [<fleet-name>]
|
|
#
|
|
# Examples:
|
|
# scripts/fork-fleet.sh ~/development/acme-prod-management
|
|
# scripts/fork-fleet.sh /tmp/test-fork test-fleet
|
|
#
|
|
# Default fleet-name is the destination dir's basename.
|
|
|
|
set -euo pipefail
|
|
|
|
DEST="${1:?usage: $0 <destination-dir> [<fleet-name>]}"
|
|
FLEET_NAME="${2:-$(basename "$DEST" | sed 's/-management$//')}"
|
|
SRC="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
info() { printf ' %s\n' "$*"; }
|
|
|
|
if [ -e "$DEST" ]; then
|
|
echo "FATAL: $DEST already exists — refusing to overwrite" >&2
|
|
exit 1
|
|
fi
|
|
|
|
bold "→ forking $SRC"
|
|
info " destination : $DEST"
|
|
info " fleet name : $FLEET_NAME"
|
|
echo
|
|
|
|
# ── 1. Mirror the reusable tooling ──────────────────────────────────
|
|
# Excludes are explicit so adding a new fleet-specific dir to the
|
|
# source doesn't accidentally leak into forks.
|
|
mkdir -p "$DEST"
|
|
rsync -a \
|
|
--exclude='.git/' \
|
|
--exclude='.claude/' \
|
|
--exclude='stacks-mirror/' \
|
|
--exclude='servers/*' \
|
|
--exclude='stacks/*' \
|
|
--exclude='configs/' \
|
|
--exclude='docs/orientation.md' \
|
|
--exclude='docs/runbooks/' \
|
|
--exclude='docs/pfi/' \
|
|
--exclude='MEMORY.md' \
|
|
--exclude='STATUS.md' \
|
|
--exclude='README.md' \
|
|
--exclude='CLAUDE.md' \
|
|
--exclude='playbooks/deploy-*' \
|
|
--exclude='playbooks/decouple-*' \
|
|
--exclude='scripts/upgrade-irv-ml1-cuda.sh' \
|
|
"$SRC/" "$DEST/"
|
|
|
|
info "tooling copied"
|
|
|
|
# ── 2. Replace gitkeeps in the now-empty fleet dirs ─────────────────
|
|
mkdir -p "$DEST/servers" "$DEST/stacks" "$DEST/stacks-mirror" \
|
|
"$DEST/configs" "$DEST/docs/runbooks"
|
|
touch "$DEST/servers/.gitkeep" "$DEST/stacks/.gitkeep" \
|
|
"$DEST/configs/.gitkeep" "$DEST/docs/runbooks/.gitkeep"
|
|
# stacks-mirror is gitignored — no .gitkeep needed.
|
|
|
|
# ── 3. Skeleton CLAUDE.md ───────────────────────────────────────────
|
|
cat > "$DEST/CLAUDE.md" <<EOF
|
|
# CLAUDE.md
|
|
|
|
This workspace manages **${FLEET_NAME}** — servers, Docker stacks,
|
|
configs. Forked from a shared infra-management template; the
|
|
conventions below carry over, the server table + placement rules are
|
|
yours to fill in.
|
|
|
|
**New session starting here?** Once \`docs/orientation.md\` exists
|
|
read it first — fleet topology, backup architecture, governing
|
|
principles, and any host-specific gotchas.
|
|
|
|
**For SSH-driven work: use \`scripts/elway\`.** Write a playbook under
|
|
\`playbooks/<name>.yaml\` and run
|
|
\`scripts/elway <host> --playbook ...\` instead of chaining
|
|
\`ssh -t host 'sudo …'\` commands — handles sudo once lazily,
|
|
structured pass/change/fail reporting, idempotency via
|
|
\`creates:\` / \`when:\` / \`changed_when:\`. Template:
|
|
[\`playbooks/elway-smoke.yaml\`](playbooks/elway-smoke.yaml).
|
|
|
|
## Purpose
|
|
|
|
- Inventory of servers and their state
|
|
- Canonical copies of Docker Compose stacks deployed on those servers
|
|
- Scripts for inspecting and managing the infrastructure
|
|
- Conventions so all stacks look the same
|
|
|
|
This is a **reference workspace** — the authoritative copies of
|
|
compose files and configs live **on the servers** under
|
|
\`/opt/docker/compose/<stack>/\` and \`/opt/docker/conf/<stack>/\`.
|
|
This workspace mirrors them for version control, editing, and planning.
|
|
|
|
## Conventions (enforce for every new stack)
|
|
|
|
- **Compose location on server:** \`/opt/docker/compose/<stack>/compose.yaml\`
|
|
- **Config mounts on server:** \`/opt/docker/conf/<stack>/...\`
|
|
- **Networks:** external \`traefik-net\`, aliased as \`tnet\` in compose
|
|
|
|
\`\`\`yaml
|
|
networks:
|
|
tnet:
|
|
name: traefik-net
|
|
external: true
|
|
\`\`\`
|
|
|
|
- **GPU reservation:** prefer \`deploy.resources.reservations.devices\`
|
|
with explicit \`device_ids\` for pinning
|
|
|
|
\`\`\`yaml
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
device_ids: ["1"]
|
|
capabilities: [gpu]
|
|
\`\`\`
|
|
|
|
- **Tunables:** \`.env\` in the same directory as \`compose.yaml\` —
|
|
keep the compose file constant, edit the \`.env\`
|
|
- **Named volumes** for service state (pattern: \`<stack>_<name>\`)
|
|
- **Bind mounts** only for: model files, config files
|
|
(\`/opt/docker/conf/...\`), docker socket where required
|
|
- **Restart policy:** \`restart: unless-stopped\` for daemons
|
|
- **Homepage labels** on user-facing services:
|
|
|
|
\`\`\`yaml
|
|
labels:
|
|
- homepage.group=AI Systems
|
|
- homepage.name=<ServiceName>
|
|
- homepage.icon=mdi-<icon>
|
|
- homepage.description=<short>
|
|
- homepage.href=http://<host-ip>:<port>
|
|
\`\`\`
|
|
|
|
- **Healthchecks** on services that expose HTTP
|
|
|
|
## Servers
|
|
|
|
| Name | IP | Role | Details |
|
|
|------|-----|------|---------|
|
|
| _(none yet — \`scripts/add-host.sh <name> <ip>\` to register)_ | | | |
|
|
|
|
## How to refresh a server's state
|
|
|
|
\`\`\`bash
|
|
scripts/refresh-server-info.sh # show help
|
|
scripts/refresh-server-info.sh all # every host
|
|
scripts/refresh-server-info.sh <host> # one host
|
|
scripts/refresh-server-info.sh --validate-only all
|
|
\`\`\`
|
|
|
|
To register a new server:
|
|
|
|
\`\`\`bash
|
|
scripts/add-host.sh <name> <ip-or-user@ip>
|
|
scripts/refresh-server-info.sh <name>
|
|
\`\`\`
|
|
|
|
## Stack tree convention (canonical vs mirror)
|
|
|
|
| tree | role | git | who writes | who reads |
|
|
|---|---|---|---|---|
|
|
| \`stacks/<stack>/\` | **canonical / intent** | tracked | you / Claude | \`deploy-stack.sh\` |
|
|
| \`stacks-mirror/<host>/<stack>/\` | **snapshot / reality** | gitignored | \`sync-stacks.sh\` | drift inspection |
|
|
|
|
\`\`\`bash
|
|
\$EDITOR stacks/<stack>/compose.yaml
|
|
scripts/deploy-stack.sh <host> <stack> # diffs vs live, prompts y/N
|
|
scripts/sync-stacks.sh # drift check across all hosts
|
|
diff -ru stacks/<stack>/ stacks-mirror/<host>/<stack>/
|
|
\`\`\`
|
|
|
|
## Working rules
|
|
|
|
- **Copies, not symlinks.** Files here reflect what's on the server
|
|
at the time of the last sync. Editing here doesn't change the
|
|
server until you deploy.
|
|
- **Never commit secrets.** Use \`.env.example\` templates; real
|
|
\`.env\` files live on the server (gitignored).
|
|
- **Surgical edits.** When fixing one stack, don't touch unrelated ones.
|
|
- **Sanity-check before deploying.** Run \`docker compose config\`
|
|
(dry parse) before \`docker compose up -d\` on the server.
|
|
EOF
|
|
|
|
# ── 4. Skeleton README.md ───────────────────────────────────────────
|
|
cat > "$DEST/README.md" <<EOF
|
|
# ${FLEET_NAME}-management
|
|
|
|
Infrastructure-management workspace for the **${FLEET_NAME}** fleet —
|
|
inventory, canonical Docker compose stacks, deploy tooling.
|
|
|
|
Forked from the shared infra-management template; see
|
|
[\`CLAUDE.md\`](./CLAUDE.md) for conventions and tooling reference.
|
|
|
|
## Quick start
|
|
|
|
\`\`\`bash
|
|
# Register your first host
|
|
scripts/add-host.sh <name> <ip-or-user@ip>
|
|
|
|
# Pull a snapshot of its current state
|
|
scripts/refresh-server-info.sh <name>
|
|
|
|
# Smoke-test the elway runner against it
|
|
scripts/elway <name> --playbook playbooks/elway-smoke.yaml
|
|
\`\`\`
|
|
|
|
## Layout
|
|
|
|
\`\`\`
|
|
${FLEET_NAME}-management/
|
|
├── CLAUDE.md # workspace conventions + assistant guidance
|
|
├── README.md # this file
|
|
├── scripts/ # reusable tooling
|
|
│ ├── elway # SSH-driven playbook runner (lazy sudo)
|
|
│ ├── server_inspect.sh
|
|
│ ├── refresh-server-info.sh
|
|
│ ├── add-host.sh
|
|
│ ├── deploy-stack.sh
|
|
│ ├── sync-stacks.sh
|
|
│ ├── add-digest-user.sh
|
|
│ └── fork-fleet.sh # bootstrap another fleet from this one
|
|
├── playbooks/
|
|
│ └── elway-smoke.yaml # template playbook
|
|
├── servers/<name>/ # one dir per registered host
|
|
└── stacks/<stack>/ # one dir per canonical stack
|
|
\`\`\`
|
|
|
|
## Onboarding the assistant
|
|
|
|
This workspace is intended to be opened in a Claude Code session
|
|
named after the fleet (e.g. \`${FLEET_NAME}-infra\`). The
|
|
\`CLAUDE.md\` file is auto-loaded and gives the assistant fleet
|
|
conventions and tool wiring.
|
|
EOF
|
|
|
|
# ── 5. Skeleton STATUS.md (optional rolling-status doc) ─────────────
|
|
cat > "$DEST/STATUS.md" <<EOF
|
|
# Status + Open Issues
|
|
|
|
Last updated: $(date '+%Y-%m-%d')
|
|
|
|
Snapshot of fleet state and open work. Refresh when something material
|
|
changes (host added, stack deployed, incident closed).
|
|
|
|
## Fleet summary
|
|
|
|
_(none yet — populate after first \`scripts/add-host.sh\`)_
|
|
|
|
## Open work
|
|
|
|
_(empty)_
|
|
|
|
## Known issues
|
|
|
|
_(empty)_
|
|
EOF
|
|
|
|
info "skeleton CLAUDE.md / README.md / STATUS.md written"
|
|
|
|
# ── 6. Initialize fresh git history ─────────────────────────────────
|
|
cd "$DEST"
|
|
git init -q -b main
|
|
git add .
|
|
# Quiet local commit — no remote, no signing fuss. The user's normal
|
|
# git config picks up author. If the source repo has a different user
|
|
# committing here would inherit it; explicit fallback to env keeps the
|
|
# script working in CI/headless contexts where git config isn't set.
|
|
if ! git -c user.useConfigOnly=true commit -q -m "Initial commit: forked from infra-management template
|
|
|
|
Stripped fleet-specific content; kept the reusable tooling
|
|
(scripts/, elway-smoke.yaml template, conventions section of CLAUDE.md,
|
|
.gitignore). Fleet name: ${FLEET_NAME}." 2>/dev/null; then
|
|
git -c user.email=fork@local -c user.name=fork-fleet \
|
|
commit -q -m "Initial commit: forked from infra-management template
|
|
|
|
Stripped fleet-specific content; kept the reusable tooling
|
|
(scripts/, elway-smoke.yaml template, conventions section of CLAUDE.md,
|
|
.gitignore). Fleet name: ${FLEET_NAME}."
|
|
fi
|
|
|
|
bold ""
|
|
bold "✓ forked into $DEST"
|
|
echo
|
|
echo "next steps:"
|
|
echo " cd $DEST"
|
|
echo " # 1. Edit CLAUDE.md — fill in the Servers table + any placement rules"
|
|
echo " # 2. Add your first host"
|
|
echo " scripts/add-host.sh <name> <ip-or-user@ip>"
|
|
echo " scripts/refresh-server-info.sh <name>"
|
|
echo " # 3. Wire up a remote when ready"
|
|
echo " tea repo create vh/${FLEET_NAME}-management # or whatever namespace"
|
|
echo " git remote add origin git@gitea.phasefinal.com:vh/${FLEET_NAME}-management.git"
|
|
echo " git push -u origin main"
|