# Pinchflat Test Deployment Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Deploy Pinchflat (YouTube auto-archiver) on Atlantis via a hand-run docker compose on a feature branch, so the user can evaluate it before committing to production promotion.

**Architecture:** Single `ghcr.io/kieraneglin/pinchflat:latest` container, port-published on `8945` on Atlantis's LAN IP. Config on Atlantis NVMe (`/volume2/metadata/docker2/pinchflat/config`), downloads on SATA array (`/volume1/data/media/youtube`). No SSO, no reverse proxy, no Kuma monitor, no Portainer stack — purely a branch-based hand-run test.

**Tech Stack:** Docker Compose, Synology DSM (Atlantis NAS), SSH to `vish@atlantis`, git.

**Reference spec:** `docs/superpowers/specs/2026-04-24-pinchflat-design.md`

---

## File Structure

Files added to the repo on branch `feat/pinchflat`:

- `hosts/synology/atlantis/pinchflat/docker-compose.yml` — the single-service compose definition
- `docs/services/individual/pinchflat.md` — brief stub (purpose, URL, test status), following repo convention at `docs/services/individual/<service>.md`

Non-tracked host state created on Atlantis:

- `/volume2/metadata/docker2/pinchflat/config/` — persistent SQLite + YAML config
- `/volume1/data/media/youtube/` — download target folder
- `/volume1/homes/vish/pinchflat-test/` — throwaway working copy of the branch

No modifications to existing files. No Portainer stack registration.

---

## Task 1: Pre-flight checks on Atlantis

**Files:** None (verification only)

- [ ] **Step 1: Confirm port 8945 is free on Atlantis**

Run:
```bash
ssh vish@atlantis 'ss -tlnp 2>/dev/null | grep -E ":8945\b" || echo "port 8945 free"'
```

Expected: `port 8945 free`

If a process is listening on 8945, stop and re-plan — pick a different host port (e.g. 8946) and update the compose file in Task 2 accordingly.

- [ ] **Step 2: Confirm media root exists and ownership convention**

Run:
```bash
ssh vish@atlantis 'ls -ld /volume1/data/media /volume2/metadata/docker2'
```

Expected output includes two directories owned by a user account, with `/volume1/data/media` containing existing media subfolders (movies, tv, anime, etc.).

- [ ] **Step 3: Confirm Docker daemon is running on Atlantis**

Run:
```bash
ssh vish@atlantis 'docker ps --format "table {{.Names}}\t{{.Status}}" | head -5'
```

Expected: a table listing at least a handful of running containers (plex, sonarr, etc.). If Docker isn't responding, stop and investigate before proceeding.

---

## Task 2: Create feature branch and compose file

**Files:**
- Create: `hosts/synology/atlantis/pinchflat/docker-compose.yml`

- [ ] **Step 1: Create branch off main**

Run from the repo root `/home/homelab/organized/repos/homelab`:
```bash
git checkout main && git pull --ff-only && git checkout -b feat/pinchflat
```

Expected: branch `feat/pinchflat` checked out, working tree clean except for the pre-existing untracked items (`.secrets.baseline` modifications, `data/expenses.csv`, `.superpowers/`, `backups/`).

- [ ] **Step 2: Create the compose directory**

Run:
```bash
mkdir -p hosts/synology/atlantis/pinchflat
```

- [ ] **Step 3: Write `hosts/synology/atlantis/pinchflat/docker-compose.yml`**

Full file contents:

```yaml
# Pinchflat - YouTube auto-archiver (test deployment)
# Port: 8945
# Docs: https://github.com/kieraneglin/pinchflat
# Scope: lightweight evaluation on Atlantis. No SSO, no reverse proxy, no Kuma.
# See: docs/superpowers/specs/2026-04-24-pinchflat-design.md
version: "3.8"

services:
  pinchflat:
    image: ghcr.io/kieraneglin/pinchflat:latest
    container_name: pinchflat

    environment:
      - PUID=1029
      - PGID=100
      - TZ=America/Los_Angeles
      - UMASK=022

    ports:
      - "8945:8945"

    volumes:
      - /volume2/metadata/docker2/pinchflat/config:/config
      - /volume1/data/media/youtube:/downloads

    healthcheck:
      test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1:8945/ 2>/dev/null || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

    security_opt:
      - no-new-privileges:true

    restart: unless-stopped
```

- [ ] **Step 4: Validate the YAML**

Run from the repo root:
```bash
python3 -c "import yaml; yaml.safe_load(open('hosts/synology/atlantis/pinchflat/docker-compose.yml'))" && echo "YAML OK"
```

Expected: `YAML OK`

If parsing fails, fix the indentation/syntax before proceeding. Do not use `sed` to edit — re-run the Write step with the full corrected file.

---

## Task 3: Write the docs stub

**Files:**
- Create: `docs/services/individual/pinchflat.md`

- [ ] **Step 1: Write the doc stub**

Full file contents:

````markdown
# Pinchflat

YouTube channel auto-archiver. Subscribes to channels, polls for new uploads, downloads via yt-dlp, stores locally with metadata/subtitles/chapters.

## Status

**Test deployment as of 2026-04-24.** Running on Atlantis via hand-run `docker compose` on branch `feat/pinchflat`. Not yet registered in Portainer, not yet behind Authentik / NPM / Kuma.

See `docs/superpowers/specs/2026-04-24-pinchflat-design.md` for design rationale and promotion path.

## Access

- **Web UI:** http://192.168.0.200:8945 (LAN only)
- **Host:** Atlantis
- **Image:** `ghcr.io/kieraneglin/pinchflat:latest`

## Paths on Atlantis

- **Compose:** `/volume1/homes/vish/pinchflat-test/hosts/synology/atlantis/pinchflat/docker-compose.yml` (during test)
- **Config:** `/volume2/metadata/docker2/pinchflat/config`
- **Downloads:** `/volume1/data/media/youtube/<Channel>/<YYYY-MM-DD> - <Title>.mkv`

## Runtime defaults (configure in web UI on first launch)

- Output template: `/downloads/{{ source_custom_name }}/{{ upload_yyyy_mm_dd }} - {{ title }}.{{ ext }}`
- Resolution cap: 2160p (4K)
- Container format: MKV (required for VP9/AV1 4K streams)
- Thumbnails: on
- Subtitles: on
- Chapters: on
- NFO files: off

## Operations

### Start / stop (during test phase)

```bash
ssh vish@atlantis
cd /volume1/homes/vish/pinchflat-test
docker compose up -d     # start
docker compose logs -f   # tail logs
docker compose down      # stop (data preserved)
```

### Full teardown (abandon test)

```bash
ssh vish@atlantis
cd /volume1/homes/vish/pinchflat-test
docker compose down -v
sudo rm -rf /volume2/metadata/docker2/pinchflat /volume1/data/media/youtube
cd ~ && rm -rf /volume1/homes/vish/pinchflat-test
```

Then on the workstation:
```bash
git branch -D feat/pinchflat
git push origin --delete feat/pinchflat   # if pushed
```

## Promotion to production (if keeping)

1. Merge `feat/pinchflat` → `main`.
2. Register new Portainer GitOps stack pointing at `hosts/synology/atlantis/pinchflat/docker-compose.yml`.
3. Stop the hand-run container, re-up via Portainer.
4. Add NPM proxy host `pinchflat.vish.gg` + Authentik proxy provider.
5. Add Kuma HTTP monitor against `http://192.168.0.200:8945`.
6. Pin the image to a specific digest instead of `:latest`.
7. Expand this doc with operational runbook (channel subscription process, troubleshooting, log locations).
````

- [ ] **Step 2: Verify the doc renders as expected Markdown**

Run:
```bash
head -5 docs/services/individual/pinchflat.md && echo "---" && wc -l docs/services/individual/pinchflat.md
```

Expected: the first line is `# Pinchflat`, line count is around 60–70.

---

## Task 4: Commit and push the branch

**Files:** Both files from Task 2 and Task 3.

- [ ] **Step 1: Stage the two new files only**

Explicitly avoid staging the pre-existing dirty working-tree items (`.secrets.baseline`, `data/expenses.csv`, `.superpowers/`, `backups/`).

Run:
```bash
git add hosts/synology/atlantis/pinchflat/docker-compose.yml docs/services/individual/pinchflat.md
git status --short
```

Expected: only the two new files appear in the "to be committed" section. Other items remain in the unstaged/untracked section untouched.

- [ ] **Step 2: Commit**

Run:
```bash
git commit -m "feat(pinchflat): add test deployment compose + docs stub

Hand-run docker compose on Atlantis port 8945 for evaluating Pinchflat as a
YouTube auto-archiver. No SSO/NPM/Kuma/Portainer during the test phase.

See docs/superpowers/specs/2026-04-24-pinchflat-design.md for design and
promotion path."
```

Expected: a commit hook chain runs (trim whitespace, check yaml, yamllint, docker-compose syntax check, detect-secrets). All should pass. Per CLAUDE.md, never add `Co-Authored-By` lines.

If yamllint fails on the new compose file, read the error, fix the compose file inline with Edit (not sed), re-validate per Task 2 Step 4, then `git add` + `git commit --amend --no-edit` is acceptable here since the commit has not been pushed yet.

- [ ] **Step 3: Push the branch**

Run:
```bash
git push -u origin feat/pinchflat
```

Expected: branch published to Gitea. Output shows tracking established.

---

## Task 5: Deploy on Atlantis

**Files:** None in the repo; creates host directories and launches the container.

- [ ] **Step 1: Create the config and download directories with correct ownership**

Pinchflat runs as `PUID=1029 PGID=100` (Synology `dockerlimited:users`). Pre-create the dirs so there's no first-boot permission confusion.

Run:
```bash
ssh vish@atlantis 'sudo mkdir -p /volume2/metadata/docker2/pinchflat/config /volume1/data/media/youtube && sudo chown -R 1029:100 /volume2/metadata/docker2/pinchflat /volume1/data/media/youtube && ls -ld /volume2/metadata/docker2/pinchflat /volume1/data/media/youtube'
```

Expected: both directories exist and are owned by `dockerlimited:users` (which may display as `1029:users` depending on DSM's /etc/passwd).

- [ ] **Step 2: Clone the branch to a throwaway working copy on Atlantis**

Run:
```bash
ssh vish@atlantis 'mkdir -p /volume1/homes/vish/pinchflat-test && cd /volume1/homes/vish/pinchflat-test && git clone --branch feat/pinchflat --depth 1 https://git.vish.gg/vish/homelab.git . && ls hosts/synology/atlantis/pinchflat/'
```

Expected: a single `docker-compose.yml` listed under the pinchflat directory. If the git clone fails (auth or network), fall back to `scp` of the single compose file:
```bash
scp hosts/synology/atlantis/pinchflat/docker-compose.yml vish@atlantis:/volume1/homes/vish/pinchflat-test/docker-compose.yml
```

(in which case the `docker compose` commands in later steps should be run directly from `/volume1/homes/vish/pinchflat-test/` rather than the nested path)

- [ ] **Step 3: Pull the image**

Run:
```bash
ssh vish@atlantis 'cd /volume1/homes/vish/pinchflat-test/hosts/synology/atlantis/pinchflat && docker compose pull'
```

Expected: image `ghcr.io/kieraneglin/pinchflat:latest` pulls successfully. If pull fails on auth (ghcr.io rate-limit), add `docker login ghcr.io` with a PAT and retry.

- [ ] **Step 4: Start the container**

Run:
```bash
ssh vish@atlantis 'cd /volume1/homes/vish/pinchflat-test/hosts/synology/atlantis/pinchflat && docker compose up -d'
```

Expected: `Container pinchflat  Started`. No errors.

- [ ] **Step 5: Verify container status**

Run:
```bash
ssh vish@atlantis 'docker ps --filter name=pinchflat --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
```

Expected: single row showing `pinchflat  Up N seconds (health: starting)  0.0.0.0:8945->8945/tcp, :::8945->8945/tcp`. Wait ~60 seconds after the first `up` so the healthcheck can transition from `starting` to `healthy`.

---

## Task 6: Smoke test

**Files:** None (evaluation only)

- [ ] **Step 1: Confirm the web UI responds**

Run from the workstation:
```bash
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.0.200:8945/
```

Expected: `200` (Pinchflat serves its dashboard at `/`). If you get `000` (connection refused), wait 30 seconds and retry — the app takes a moment to bind after container start.

- [ ] **Step 2: Tail logs for any obvious errors**

Run:
```bash
ssh vish@atlantis 'docker logs pinchflat --tail 50 2>&1 | grep -iE "error|fatal|panic" | head -20 || echo "no errors in recent logs"'
```

Expected: `no errors in recent logs`, or if any errors appear, assess whether they're benign (e.g. migration notices) vs blocking.

- [ ] **Step 3: Check container healthcheck transitioned to healthy**

Run:
```bash
ssh vish@atlantis 'docker inspect pinchflat --format "{{.State.Health.Status}}"'
```

Expected: `healthy`. If still `starting` after 2 minutes, check logs for why the web server isn't responding. If `unhealthy`, something is wrong — stop and debug before handing off.

- [ ] **Step 4: Confirm config and download volumes wrote correctly**

Run:
```bash
ssh vish@atlantis 'ls -la /volume2/metadata/docker2/pinchflat/config/ | head -10 && echo "---" && ls -la /volume1/data/media/youtube/'
```

Expected: the config dir now contains Pinchflat's initial files (e.g. database file, YAML config). The downloads dir may still be empty — that's fine, it only populates after the user subscribes to a channel.

- [ ] **Step 5: Hand off to user for UI walk-through**

Report to the user:
- URL: `http://192.168.0.200:8945`
- Container state: running + healthy
- Next step is user-driven: open the URL, walk through first-run setup, apply the defaults from §4 of the spec (output template, 4K cap, MKV container, thumbnails/subtitles/chapters on, NFO off), subscribe to 2–5 test channels.

Do NOT mark the plan as complete at this step. The user drives the evaluation window; the plan is complete when the user makes the keep/drop decision.

---

## Task 7 (deferred): Keep-or-drop decision

**Trigger:** User signals "keep" or "drop" after evaluation window.

**If KEEP:**
- Open follow-up plan for promotion (register Portainer GitOps stack, add NPM + Authentik + Kuma, pin image digest, expand docs).
- Out of scope for this plan.

**If DROP:**
- [ ] Run the teardown block from `docs/services/individual/pinchflat.md` (Full teardown section).
- [ ] Delete the remote branch: `git push origin --delete feat/pinchflat`.
- [ ] Delete the local branch: `git branch -D feat/pinchflat`.
- [ ] Optionally delete the two repo files with a follow-up commit on main, or leave the branch deleted and nothing ever landed on main — either is fine.

---

## Self-review notes

- **Spec coverage:** Every spec section (architecture, components, storage, runtime defaults, deployment workflow, error handling, testing, rollback) maps to a task or is explicitly deferred. ✓
- **Placeholder scan:** No TBDs. No "similar to Task N" shortcuts. Every compose field and command is literal. ✓
- **Type consistency:** Paths match across tasks (`/volume1/data/media/youtube`, `/volume2/metadata/docker2/pinchflat/config`, `/volume1/homes/vish/pinchflat-test`, port `8945`). Ownership `1029:100` consistent. ✓
- **Known fuzziness:** "60-70 lines" for the doc stub is approximate; the exact line count depends on final whitespace. Not a blocker.
