Merge branch 'dev'

This commit is contained in:
Torres
2026-11-12 09:59:45 -04:00
6 changed files with 130 additions and 8 deletions

4
.gitignore vendored
View File

@@ -4,6 +4,10 @@ data/
*.sqlite3-wal
*.sqlite3-shm
# deploy state (stack-local, never committed)
.env
tailscale-state/
# python
__pycache__/
*.pyc

View File

@@ -1,15 +1,18 @@
# Bike-Tinder app container (Phase 2 build).
# The compose stack, systemd unit and Tailscale sidecar belong to the parent —
# Bike-Tinder app container (Phase 4 deploy).
# The compose stack, systemd unit and Tailscale sidecar live beside this file —
# this file is only the app image contract.
FROM python:3.13-slim
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
# Data lands on the stack volume: /data/bike-tinder.sqlite3 and /data/img/
# Data lands on the stack volume: /data/bike-tinder.sqlite3 and /data/img/.
# /state is the hunt state dir (bind-mounted read-only) used by POST /api/import.
EXPOSE 8000
CMD ["python", "-m", "app.serve", "--host", "0.0.0.0", "--port", "8000", "--db", "/data/bike-tinder.sqlite3", "--img-dir", "/data/img"]
CMD ["python", "-m", "app.serve", "--host", "0.0.0.0", "--port", "8000", \
"--db", "/data/bike-tinder.sqlite3", "--img-dir", "/data/img", \
"--state-dir", "/state"]

64
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,64 @@
# Bike-Tinder stack — deck app + Tailscale sidecar.
#
# Deployed on Rotarran at /var/mnt/DATA/podman-stacks/bike-tinder/ (a checkout
# of the `release` branch). Run via the user unit podman-stack-bike-tinder.service
# (Delegate=yes) or manually:
# podman-compose -f deploy/docker-compose.yml --env-file .env up -d
#
# Networking:
# - app: published on 127.0.0.1:9453 only — Hermes cron triggers (import /
# agent read) and local debugging. The LAN sees nothing.
# - tailscale (userspace mode, no tun/caps): joins the tailnet as `biketinder`
# and serves https://biketinder.tailf35a34.ts.net -> http://app:8000 (443).
# Secrets: TS_AUTHKEY + OPENROUTER_API_KEY come from the stack-local .env
# (never committed). Before the authkey is placed, the sidecar simply reports
# "no authkey" and exits — the app runs fine on its own.
x-logging: &default-logging
driver: "json-file"
options:
max-size: "50m"
max-file: "5"
compress: "true"
services:
app:
build:
context: ..
dockerfile: deploy/Dockerfile
image: localhost/bike-tinder:latest
container_name: bike-tinder-app
restart: unless-stopped
environment:
- TZ=America/Halifax
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
volumes:
- /var/mnt/DATA/podman-stacks/bike-tinder/data:/data
- /var/home/admin/.hermes/scripts/motorcycle-hunt.d:/state:ro,z
ports:
- "127.0.0.1:9453:8000"
stop_grace_period: 15s
security_opt:
- no-new-privileges:true
logging:
<<: *default-logging
tailscale:
image: docker.io/tailscale/tailscale:latest
container_name: bike-tinder-ts
hostname: biketinder
restart: unless-stopped
depends_on:
- app
environment:
- TS_AUTHKEY=${TS_AUTHKEY:-}
- TS_HOSTNAME=biketinder
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve.json
- TS_USERSPACE=true
- TZ=America/Halifax
volumes:
- /var/mnt/DATA/podman-stacks/bike-tinder/tailscale-state:/var/lib/tailscale
- /var/mnt/DATA/podman-stacks/bike-tinder/deploy/tailscale:/config:ro
logging:
<<: *default-logging

View File

@@ -0,0 +1,18 @@
# /var/home/admin/.config/systemd/user/podman-stack-bike-tinder.service
# (installed from the repo: deploy/podman-stack-bike-tinder.service)
[Unit]
Description=Bike-Tinder podman-compose stack (deck app + Tailscale sidecar)
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
Delegate=yes
WorkingDirectory=/var/mnt/DATA/podman-stacks/bike-tinder
ExecStart=/var/home/admin/.local/bin/podman-compose -f deploy/docker-compose.yml --env-file .env up -d
ExecStop=/var/home/admin/.local/bin/podman-compose -f deploy/docker-compose.yml --env-file .env down
TimeoutStartSec=600
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,10 @@
{
"TCP": { "443": { "HTTPS": true } },
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": { "Proxy": "http://app:8000" }
}
}
}
}

View File

@@ -116,6 +116,29 @@ data/ runtime state (gitignored)
docs/ this design set
```
## Runbook (completed at Phase 4)
## Runbook (Phase 4)
Deploy / update / rollback steps, log locations, import troubleshooting (gates), backup-restore drill, phone verification checklist.
**Stack home:** `/var/mnt/DATA/podman-stacks/bike-tinder/` — a checkout of the `release` branch, plus stack-local `data/` (DB + image mirror + backups), `.env` (secrets, never committed) and `tailscale-state/` (node identity). Unit: `podman-stack-bike-tinder.service` (installed from `deploy/`).
**Deploy / update a cut:**
```sh
# from the repo: merge dev -> main, then fast-forward release
git -C /var/mnt/DATA/podman-stacks/bike-tinder pull --ff-only
podman-compose -f deploy/docker-compose.yml --env-file .env build
systemctl --user restart podman-stack-bike-tinder.service
```
Docs-only cuts stop after the `pull`. The unit is `oneshot` + `RemainAfterExit` with `Delegate=yes`, so the stack survives `hermes gateway restart` and reboots. The historical dev service (`bike-tinder-dev.service`) is retired at deploy; rollback happens through the stack, not that unit.
**Rollback:** check out the previous release SHA in the stack dir, rebuild, restart. The DB is never touched by code rollbacks (migrations are guarded and idempotent); if a migration ever needs unwinding, restore the last backup (below).
**Logs:** `podman logs bike-tinder-app`, `podman logs bike-tinder-ts`, `journalctl --user -u podman-stack-bike-tinder.service`.
**Import troubleshooting:** `POST /api/import` refuses when the hunt state is older than 26 h or missing — check `~/.hermes/scripts/motorcycle-hunt.d/*.json`. The nightly chain is the Hermes cron job `bike-tinder-import` (08:30 Atlantic: import → agent read → DB backup); it is silent on success by design — inspect its last run in the cron list if a morning looks stale. The container also runs an on-boot catch-up import when the last run is >26 h old.
**Backups:** nightly `data/backup/bike-tinder-YYYYMMDD-HHMM.sqlite3`, last 14 kept. Restore drill: stop the unit → remove `data/bike-tinder.sqlite3-wal`/`-shm` → copy the chosen backup over `data/bike-tinder.sqlite3` → start the unit → check `GET /api/stats`.
**Phone checklist:** open `https://biketinder.tailf35a34.ts.net` from a tailnet device → expect the deck; Add to Home Screen (PWA installs via the manifest); swipe once and confirm the decision shows in List. If the URL fails: check the sidecar (`podman logs bike-tinder-ts`), confirm `TS_AUTHKEY` in `.env`, and re-run `podman-compose -f deploy/docker-compose.yml --env-file .env up -d`.
**First-run sidecar (one-time):** mint a Tailscale auth key (admin console → Settings → Keys), put `TS_AUTHKEY=<key>` into the stack `.env`, then `podman-compose -f deploy/docker-compose.yml --env-file .env up -d tailscale`.