{
 "schema_version": 1,
 "title": "A River Becoming: system map",
 "updated": "2026-10-18T18:10:00-07:00",
 "about": "Every machine, device, service and connection of A River Becoming, for people and agents. Source of truth for docs/system/index.html (interactive) and docs/system/system.md (generated by tools/system_docs.py). Validated by tools/tests/test_system_json.py. Optional display fields: node.short (diagram name) and link.label (text on the arrow).",
 "rules": [
  "Everything except secrets: names, hostnames, LAN and Tailscale IPs, ports, usernames and account emails are in; passwords and token values never (Gavin, 18 Oct).",
  "Secrets appear only by name and where they live.",
  "The river site is given as an area only.",
  "Never advertise a site's own LAN as a Tailscale subnet from another device: it hijacks bitcube's local traffic (18 Oct, when the GL.iNet's 10.0.7.0/24 route took the gallery cam and the eero away from bitcube).",
  "verified: confirmed = checked on the device, in code, or by the team; unverified = believed but not checked; awaiting:<Name> = needs that person's answer."
 ],
 "enums": {
  "status": {
   "working": "running as described",
   "partial": "works with gaps",
   "standby": "idle by design until needed",
   "planned": "agreed or proposed, not built",
   "blocked": "waiting on someone",
   "broken": "should work and doesn't",
   "stopped": "exists, deliberately off",
   "unknown": "state not known"
  },
  "verified": [
   "confirmed",
   "unverified",
   "awaiting:Dave",
   "awaiting:Will",
   "awaiting:Andi",
   "awaiting:Gavin"
  ],
  "medium": {
   "artnet": "Art-Net on the lighting Ethernet (2.0.0.0/8)",
   "ethernet": "other Ethernet between lighting devices",
   "xlr": "4-pin XLR, 24 V + SPI data to a rope",
   "video": "HDMI, USB-C display, Colorlight and HUB75 video chain",
   "lan": "a local network (Wi-Fi or Ethernet) at a site",
   "tailscale": "over a tailnet (WireGuard)",
   "https": "HTTPS over the internet",
   "git": "git pushes, Pages builds, GitHub Actions",
   "starlink": "Starlink satellite uplink",
   "internal": "inside Modal"
  },
  "kind": [
   "computer",
   "display",
   "led-controller",
   "rope",
   "camera",
   "router",
   "network",
   "power",
   "service",
   "tailnet",
   "cloud-app",
   "endpoint",
   "storage",
   "repo",
   "website"
  ]
 },
 "scenarios": [
  {
   "id": "now",
   "name": "Now: Agape",
   "when": "18 Oct 2026",
   "status": "working",
   "summary": "Prototype at Agape. bitcube runs the local rain checkpoint on 18 of 20 ropes and HUB75 quad 1, with no Modal input yet. The gallery cam is on Agape WiFi, and bitcube shares it on Will's tailnet (10.0.7.76/32). The GL.iNet router and the river cam left for the river with Colin on the evening of 18 Oct. Modal's agent ticks every 5 minutes, but nothing on bitcube reads it yet."
  },
  {
   "id": "show",
   "name": "Show: Gray Area",
   "when": "install from Wed 22 Oct 2026, show to 27 Oct",
   "status": "planned",
   "summary": "Planned and partly unsettled. bitcube and all lighting move to Gray Area: lighting NIC on a switch to the rack and PSAC10s, internet from the gallery network on the other NIC or Wi-Fi. The gallery cam joins the gallery Wi-Fi and bitcube advertises its address on Will's tailnet, as at Agape. bitcube mirrors Modal's scene (sync.py, :8770) and pushes gallery clips to /ingest. The river cam is at the river on Starlink.",
   "open_questions": [
    "q-gallery-network",
    "q-gallery-cam-ip",
    "q-river-capture",
    "q-ingest-names",
    "q-site-token",
    "q-sync-ok",
    "q-fallback",
    "q-td-licence"
   ]
  }
 ],
 "places": [
  {
   "id": "river",
   "name": "The River",
   "kind": "site",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "partial",
   "verified": "unverified",
   "summary": "The real river the agent watches. The GL.iNet router and the Reolink E1 Outdoor Pro went there with Colin on 18 Oct; Starlink and solar/battery power are expected there (unverified).",
   "area": "near Dobbins, California",
   "notes": [
    "Location given as an area only, on purpose: the kit there is unattended."
   ],
   "sources": [
    "signal",
    "river-agent",
    "session-a"
   ]
  },
  {
   "id": "agape",
   "name": "Agape (prototype)",
   "kind": "site",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "summary": "Gavin's house in the Mission, SF, where everything is prototyped until the Gray Area install. Gavin's Mac mini stays here.",
   "network": {
    "lan": "10.0.7.0/24",
    "gateway": "10.0.7.1 (eero)",
    "wifi": "Agape WiFi"
   },
   "sources": [
    "ssh-bitcube",
    "log"
   ]
  },
  {
   "id": "grayarea",
   "name": "Gray Area (show)",
   "kind": "site",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "summary": "The gallery. Install Wed 22 to Mon 27 Oct 2026. Network plan not settled (open questions to Will).",
   "address": "Gray Area, 2665 Mission St, San Francisco",
   "sources": [
    "log",
    "plan"
   ]
  },
  {
   "id": "modal",
   "name": "Modal cloud",
   "kind": "cloud",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "summary": "Dave's agent: one Modal app (tick, watch, web) plus model apps, in workspace to-be-a-river.",
   "sources": [
    "river-agent",
    "modal-live"
   ]
  },
  {
   "id": "internet",
   "name": "Internet and Tailscale",
   "kind": "network",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "summary": "The public internet, the two tailnets, and team devices that aren't tied to a site.",
   "sources": [
    "tailscale-bitcube",
    "tailscale-mini"
   ]
  },
  {
   "id": "github",
   "name": "GitHub",
   "kind": "cloud",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "summary": "The two repos, the public status site (GitHub Pages) and the camera snapshot branch.",
   "sources": [
    "log"
   ]
  }
 ],
 "nodes": [
  {
   "id": "bitcube",
   "name": "bitcube",
   "kind": "computer",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "will",
   "status": "working",
   "verified": "confirmed",
   "summary": "The show machine: a Windows mini PC that runs TouchDesigner, drives the ropes (Art-Net) and the HUB75 (HDMI), reads the cameras, and will mirror Modal's scene.",
   "specs": {
    "os": "Windows 11 Pro 10.0.22621",
    "cpu": "Intel Core i7-1185G7 (11th gen)",
    "ram": "15.8 GB",
    "gpu": "Intel Iris Xe (driver 32.0.101.7088)",
    "bios": "American Megatrends MU05P V0.14",
    "computer_name": "BITCUBE",
    "boot": "Windows Boot Manager first (Ubuntu still installed); auto-login via Sysinternals Autologon; Tailscale unattended; no sleep or hibernate on AC; restart takes about 1 minute",
    "tailscale_routes": "advertises 10.0.7.76/32 (gallery cam) on Will's tailnet; accepts other nodes' routes"
   },
   "interfaces": [
    {
     "name": "Wi-Fi",
     "hw": "Intel Wi-Fi 6 AX200",
     "mac": "70-D8-23-16-A1-5C",
     "ip": "10.0.7.79/24",
     "how": "DHCP on Agape WiFi (5 GHz, profile Private)",
     "gateway": "10.0.7.1 (default route, metric 0)",
     "scenario": "now"
    },
    {
     "name": "Ethernet",
     "hw": "Realtek PCIe GbE",
     "mac": "00-E7-43-0E-5F-C3",
     "ip": "2.0.0.8/8",
     "how": "static; the lighting (Art-Net) network",
     "link": "100 Mbps"
    },
    {
     "name": "Ethernet (second address)",
     "ip": "192.168.1.99/24",
     "how": "static, gateway 192.168.1.1 (metric 256); probably left over from the 2023 BitCube setup",
     "verified": "unverified"
    },
    {
     "name": "Ethernet 2",
     "hw": "Realtek PCIe GbE #2",
     "mac": "00-E7-43-0E-5F-C4",
     "ip": null,
     "how": "disconnected; candidate for the gallery uplink (dual-NIC plan)"
    },
    {
     "name": "Tailscale",
     "ip": "100.73.68.60",
     "ipv6": "fd7a:115c:a1e0::3c3b:443d",
     "how": "Will's tailnet tail03bc1a; also node-shared into Gavin's tailnet. Advertises 10.0.7.76/32 (the gallery cam, approved 18 Oct 17:55) and accepts routes (RouteAll)."
    }
   ],
   "hostnames": [
    "bitcube",
    "bitcube.local",
    "bitcube.tail03bc1a.ts.net"
   ],
   "accounts": [
    {
     "service": "Windows",
     "user": "user",
     "note": "Local administrator, auto-logs in. Password held by Will (never written down here)."
    },
    {
     "service": "GitHub",
     "user": "deploy key",
     "note": "Read-write deploy key for gavaiken/river-bitcube only; no personal GitHub login on bitcube"
    }
   ],
   "access": [
    {
     "method": "SSH",
     "address": "100.73.68.60 or bitcube.tail03bc1a.ts.net",
     "port": 22,
     "user": "user",
     "auth": "SSH key in C:\\ProgramData\\ssh\\administrators_authorized_keys (Gavin's mini, Dave)",
     "note": "Full admin rights, no UAC. From the mini: `ssh bitcube`."
    },
    {
     "method": "VNC",
     "address": "vnc://bitcube.tail03bc1a.ts.net (100.73.68.60)",
     "port": 5900,
     "auth": "TightVNC password",
     "note": "Shows the real console and HDMI output. Prefer this over RDP."
    },
    {
     "method": "RDP",
     "address": "100.73.68.60",
     "port": 3389,
     "user": "user",
     "auth": "Windows password",
     "note": "Takes over the console: the HUB75 output disappears while connected. Hand back with `tscon <id> /dest:console`."
    },
    {
     "method": "SMB",
     "address": "\\\\bitcube\\River, smb://100.73.68.60/River, smb://bitcube.local/River",
     "port": 445,
     "user": "user",
     "auth": "Windows password",
     "note": "Share River = C:\\Users\\user\\River"
    },
    {
     "method": "TeamViewer",
     "port": 5938,
     "note": "Service running (2023 leftover); account unverified"
    }
   ],
   "ports": [
    {
     "port": 22,
     "proto": "tcp",
     "service": "OpenSSH sshd",
     "bind": "0.0.0.0"
    },
    {
     "port": 3389,
     "proto": "tcp",
     "service": "Remote Desktop",
     "bind": "0.0.0.0"
    },
    {
     "port": 5900,
     "proto": "tcp",
     "service": "TightVNC server",
     "bind": "0.0.0.0",
     "note": "Firewall rule allows all profiles (team's choice, 17 Oct)"
    },
    {
     "port": 5800,
     "proto": "tcp",
     "service": "TightVNC HTTP viewer",
     "bind": "0.0.0.0"
    },
    {
     "port": 445,
     "proto": "tcp",
     "service": "SMB file share River"
    },
    {
     "port": 139,
     "proto": "tcp",
     "service": "NetBIOS",
     "bind": "100.73.68.60"
    },
    {
     "port": 5938,
     "proto": "tcp",
     "service": "TeamViewer service",
     "bind": "0.0.0.0"
    },
    {
     "port": 9980,
     "proto": "tcp",
     "service": "Claude to TouchDesigner bridge (WebServer DAT)",
     "bind": "0.0.0.0",
     "note": "Answers localhost only (403 to anyone else)"
    },
    {
     "port": 6454,
     "proto": "udp",
     "service": "Art-Net out (TouchDesigner)",
     "bind": "2.0.0.8"
    },
    {
     "port": 8765,
     "proto": "tcp",
     "service": "RopeLights web UI (Will)",
     "bind": "100.73.68.60",
     "note": "Only while RopeLights runs (stopped now); firewall opens it on Tailscale only"
    },
    {
     "port": 8770,
     "proto": "tcp",
     "service": "gallery/sync.py local mirror of Modal",
     "bind": "127.0.0.1",
     "status": "planned"
    },
    {
     "port": 41641,
     "proto": "udp",
     "service": "Tailscale (WireGuard)"
    }
   ],
   "services": [
    {
     "name": "TouchDesigner 2025.33230",
     "status": "working",
     "detail": "Non-Commercial licence (TOPs max 1280x1280). Login autostart Start-River.cmd (10 s) opens river-local-oct-18.toe, the local rain checkpoint. river.toe is the general show file for Dave's visuals."
    },
    {
     "name": "Art-Net sender",
     "status": "working",
     "detail": "td/ropes/artnet_out.py inside TD: 40 fps target (about 35 sent), brightness 0.4 cap, 600 W per box limiter. Byte-identical to Will's RopeLights. Keeps sending black to keep the ropes dark."
    },
    {
     "name": "HUB75 window",
     "status": "working",
     "detail": "/project1/panels: a black 1920x1080 window on the LED display (found by EDID name), panel atlas pinned at its top-left (quad 1 at 0,0; quad 2 at 256,0). In river-local-oct-18.toe a gain stage brightens quad 1 (Panelgain 3.0, Panelgamma 0.7, panels Brightness 1.0; 18 Oct 17:55)."
    },
    {
     "name": "Claude bridge",
     "status": "working",
     "detail": "td/bridge: WebServer DAT on :9980, POST /exec, GET /errors, GET /screenshot. `tools\\td.ps1` uses it."
    },
    {
     "name": "RopeLights (Will)",
     "status": "stopped",
     "detail": "Scheduled task RopeLights, no trigger. Never run it while TD sends Art-Net."
    },
    {
     "name": "RiverSnapshot",
     "status": "working",
     "detail": "Scheduled task: every 15 min, 08:00 to 02:00, a 640x360 gallery-cam still force-pushed to branch snapshot."
    },
    {
     "name": "Status page at login",
     "status": "working",
     "detail": "Start-Status.cmd (40 s): Chrome on the M14 with the public status page (local docs if offline)."
    },
    {
     "name": "Claude Code at login",
     "status": "working",
     "detail": "Start-Claude.cmd (20 s): Windows Terminal in C:\\Users\\user\\River running Claude."
    },
    {
     "name": "gallery/sync.py mirror",
     "status": "planned",
     "detail": "Session D: mirror Modal's /data files every 3 s, serve on 127.0.0.1:8770 for TD's Web Render TOP. Waiting on Dave."
    },
    {
     "name": "capture/capture.py",
     "status": "planned",
     "detail": "Session E: push 10 s gallery-cam clips to Modal /ingest. Waiting on Dave (INGEST_TOKEN, names)."
    },
    {
     "name": "Tailscale",
     "status": "working",
     "detail": "Unattended (connects before login); node key expires 2027-04-11."
    }
   ],
   "paths": {
    "repo": "C:\\Users\\user\\River\\river-bitcube",
    "dave_repo": "C:\\Users\\user\\River\\river-agent (copy; no GitHub credentials for it on bitcube)",
    "share": "C:\\Users\\user\\River",
    "td": "C:\\Program Files\\Derivative\\TouchDesigner\\bin\\TouchDesigner.exe",
    "ropelights": "C:\\Users\\user\\RopeLights",
    "secrets": "C:\\Users\\user\\River\\river-bitcube\\secrets.local.json (untracked)"
   },
   "displays": [
    "M14 (USB-C, primary, desktop and status page)",
    "LED 1920X1080 (HDMI, the Colorlight sender)"
   ],
   "sources": [
    "ssh-bitcube",
    "tailscale-bitcube",
    "log",
    "claude-md",
    "session-a"
   ]
  },
  {
   "id": "m14",
   "name": "M14 monitor",
   "kind": "display",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "Lenovo M14 portable monitor on USB-C: bitcube's primary display, so the desktop and taskbar stay off the panels.",
   "specs": {
    "edid": "LEN61DD, name M14",
    "mode": "1366x768 as Windows reports it (18 Oct)",
    "role": "Primary display; Chrome shows the status page here at login"
   },
   "notes": [
    "At Gray Area: unverified whether this monitor goes too."
   ],
   "owner_verified": "unverified",
   "sources": [
    "ssh-bitcube",
    "claude-md"
   ]
  },
  {
   "id": "colorlight-sender",
   "name": "Colorlight sender",
   "kind": "led-controller",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "andi",
   "status": "working",
   "verified": "confirmed",
   "summary": "LED video sender. Looks like a 1920x1080 HDMI display to bitcube and samples its top-left region for the receive card.",
   "specs": {
    "edid_name": "LED 1920X1080",
    "input": "HDMI 1920x1080",
    "used_region": "top-left 512x128 (two quads)",
    "config": "Andi's LEDVision/LEDSetting from their Windows PC",
    "model": "unverified"
   },
   "sources": [
    "hardware-doc",
    "ssh-bitcube"
   ]
  },
  {
   "id": "receive-card",
   "name": "Colorlight 5A-75B receive card",
   "kind": "led-controller",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "andi",
   "status": "working",
   "verified": "confirmed",
   "summary": "Receive card that drives the HUB75 panels from the sender's stream.",
   "specs": {
    "model": "Colorlight 5A-75B",
    "layout": "Andi's receive-card config; quad 2 layout still open"
   },
   "sources": [
    "hardware-doc"
   ],
   "short": "5A-75B receive card"
  },
  {
   "id": "hub75-quad1",
   "name": "HUB75 quad 1",
   "kind": "display",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "andi",
   "status": "working",
   "verified": "confirmed",
   "summary": "Four 128x64 panels as a 2x2 quad (256x128). Verified by camera: red TL with the quad-1 marker, green TR, blue BL, yellow BR.",
   "specs": {
    "panels": "4 x 128x64",
    "pixels": "256x128",
    "hdmi_region": "x 0-255, y 0-127",
    "now_showing": "the rain checkpoint (river-local-oct-18.toe), brightened 18 Oct 17:55 (Panelgain 3.0, Panelgamma 0.7)"
   },
   "sources": [
    "log",
    "plan"
   ]
  },
  {
   "id": "hub75-quad2",
   "name": "HUB75 quad 2",
   "kind": "display",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "andi",
   "status": "blocked",
   "verified": "awaiting:Andi",
   "summary": "Second quad, reserved at HDMI x 256-511. Not connected yet: waiting on Andi's receive-card layout. Blanked in TD.",
   "specs": {
    "panels": "4 x 128x64",
    "pixels": "256x128",
    "hdmi_region": "x 256-511, y 0-127"
   },
   "open_questions": [
    "q-quad2"
   ],
   "sources": [
    "plan"
   ]
  },
  {
   "id": "hub75-more",
   "name": "Other HUB75 panels",
   "kind": "display",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "andi",
   "status": "planned",
   "verified": "unverified",
   "summary": "About 30 HUB75 panels exist in total (the 'pools'); how many go into the show beyond quads 1 and 2 is not settled.",
   "specs": {
    "total_panels": "about 30 x 128x64",
    "chromatik_model": "8 pools (Gavin's 7 Oct model)"
   },
   "sources": [
    "hardware-doc",
    "log"
   ]
  },
  {
   "id": "psac10-1",
   "name": "PSAC10 box 1",
   "kind": "led-controller",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "AoShow AS_PSAC10 Art-Net node and 1,750 W PSU. Ropes 1-10 on OUT1-OUT10.",
   "specs": {
    "model": "AoShow AS_PSAC10 BOX_SPI_60U_1750W",
    "oled": "AS_832M-16P, ID:001",
    "outputs": "10 x 4-pin XLR (24 V + SPI data), 175 W each",
    "universes": "0-39 (4 per rope, 3 used)",
    "mains": "voltage selector set to 110 V",
    "power_note": "10 ropes full white = 1,500 W; TD caps at 600 W per box"
   },
   "interfaces": [
    {
     "name": "NET1/NET2 (Ethercon)",
     "ip": "2.0.0.10/8",
     "mac": "0E-FE-65-FF-02-00",
     "how": "fixed by the box"
    }
   ],
   "notes": [
    "When Art-Net stops, the box (or the rack's SD card) plays its own colour cycle. TD keeps sending black to keep it dark."
   ],
   "owner_verified": "unverified",
   "sources": [
    "hardware-doc",
    "rope-config",
    "ssh-bitcube",
    "log"
   ]
  },
  {
   "id": "psac10-2",
   "name": "PSAC10 box 2",
   "kind": "led-controller",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "Second AS_PSAC10. Ropes 11-20 on OUT1-OUT10.",
   "specs": {
    "model": "AoShow AS_PSAC10 BOX_SPI_60U_1750W",
    "oled": "ID:002",
    "outputs": "10 x 4-pin XLR (24 V + SPI data)",
    "universes": "40-79"
   },
   "interfaces": [
    {
     "name": "NET1/NET2 (Ethercon)",
     "ip": "2.0.0.11/8",
     "mac": "0E-FE-65-E2-02-00",
     "how": "fixed by the box"
    }
   ],
   "owner_verified": "unverified",
   "sources": [
    "hardware-doc",
    "rope-config",
    "ssh-bitcube"
   ]
  },
  {
   "id": "rack-controller",
   "name": "Rack LED controller",
   "kind": "led-controller",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "standby",
   "verified": "confirmed",
   "summary": "AoShow 1U master player (AS_MSDMX). Plays its SD-card program (program 2 lights the ropes) only when no Art-Net arrives.",
   "specs": {
    "model": "AoShow AS_MSDMX 1U LED Controller",
    "lcd": "P:002/005 S:07, IC:UCS1903_UCS1909_UCS1912",
    "rear": "NET1/NET2, DMX512 (XLR and RJ45), audio in, mic, wall pad RJ45, IEC 100-240 V",
    "ip": "unknown"
   },
   "notes": [
    "The river cam sits on top of it at Agape (18 Oct)."
   ],
   "owner_verified": "unverified",
   "sources": [
    "hardware-doc",
    "plan"
   ]
  },
  {
   "id": "rope-01",
   "name": "Rope 1",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT1, universes 0-2.",
   "specs": {
    "box": 1,
    "out": 1,
    "universes": [
     0,
     1,
     2
    ],
    "reserved_universe": 3,
    "config_index": 0,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "right",
    "camera_visible_px": 99,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-02",
   "name": "Rope 2",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT2, universes 4-6.",
   "specs": {
    "box": 1,
    "out": 2,
    "universes": [
     4,
     5,
     6
    ],
    "reserved_universe": 7,
    "config_index": 1,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "centre",
    "camera_visible_px": 118,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-03",
   "name": "Rope 3",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT3, universes 8-10.",
   "specs": {
    "box": 1,
    "out": 3,
    "universes": [
     8,
     9,
     10
    ],
    "reserved_universe": 11,
    "config_index": 2,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "centre",
    "camera_visible_px": 141,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-04",
   "name": "Rope 4",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT4, universes 12-14.",
   "specs": {
    "box": 1,
    "out": 4,
    "universes": [
     12,
     13,
     14
    ],
    "reserved_universe": 15,
    "config_index": 3,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "right",
    "camera_visible_px": 53,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-05",
   "name": "Rope 5",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT5, universes 16-18.",
   "specs": {
    "box": 1,
    "out": 5,
    "universes": [
     16,
     17,
     18
    ],
    "reserved_universe": 19,
    "config_index": 4,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "centre",
    "camera_visible_px": 201,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-06",
   "name": "Rope 6",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT6, universes 20-22.",
   "specs": {
    "box": 1,
    "out": 6,
    "universes": [
     20,
     21,
     22
    ],
    "reserved_universe": 23,
    "config_index": 5,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "centre",
    "camera_visible_px": 142,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-07",
   "name": "Rope 7",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT7, universes 24-26.",
   "specs": {
    "box": 1,
    "out": 7,
    "universes": [
     24,
     25,
     26
    ],
    "reserved_universe": 27,
    "config_index": 6,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "centre",
    "camera_visible_px": 77,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-08",
   "name": "Rope 8",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT8, universes 28-30.",
   "specs": {
    "box": 1,
    "out": 8,
    "universes": [
     28,
     29,
     30
    ],
    "reserved_universe": 31,
    "config_index": 7,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "right",
    "camera_visible_px": 117,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-09",
   "name": "Rope 9",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "broken",
   "verified": "confirmed",
   "summary": "Never lights (box 1 OUT9); swap test deferred, carrying on with 18 ropes.",
   "specs": {
    "box": 1,
    "out": 9,
    "universes": [
     32,
     33,
     34
    ],
    "reserved_universe": 35,
    "config_index": 8,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "not seen (dark)",
    "camera_visible_px": null,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap",
    "plan"
   ]
  },
  {
   "id": "rope-10",
   "name": "Rope 10",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 1 OUT10, universes 36-38.",
   "specs": {
    "box": 1,
    "out": 10,
    "universes": [
     36,
     37,
     38
    ],
    "reserved_universe": 39,
    "config_index": 9,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "right",
    "camera_visible_px": 207,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-1",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-11",
   "name": "Rope 11",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT1, universes 40-42.",
   "specs": {
    "box": 2,
    "out": 1,
    "universes": [
     40,
     41,
     42
    ],
    "reserved_universe": 43,
    "config_index": 10,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "left-centre",
    "camera_visible_px": 68,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-12",
   "name": "Rope 12",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT2, universes 44-46.",
   "specs": {
    "box": 2,
    "out": 2,
    "universes": [
     44,
     45,
     46
    ],
    "reserved_universe": 47,
    "config_index": 11,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "far left",
    "camera_visible_px": 184,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-13",
   "name": "Rope 13",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT3, universes 48-50.",
   "specs": {
    "box": 2,
    "out": 3,
    "universes": [
     48,
     49,
     50
    ],
    "reserved_universe": 51,
    "config_index": 12,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "left-centre",
    "camera_visible_px": 106,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-14",
   "name": "Rope 14",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT4, universes 52-54.",
   "specs": {
    "box": 2,
    "out": 4,
    "universes": [
     52,
     53,
     54
    ],
    "reserved_universe": 55,
    "config_index": 13,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "left-centre",
    "camera_visible_px": 138,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-15",
   "name": "Rope 15",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT5, universes 56-58.",
   "specs": {
    "box": 2,
    "out": 5,
    "universes": [
     56,
     57,
     58
    ],
    "reserved_universe": 59,
    "config_index": 14,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "far left",
    "camera_visible_px": 166,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-16",
   "name": "Rope 16",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT6, universes 60-62.",
   "specs": {
    "box": 2,
    "out": 6,
    "universes": [
     60,
     61,
     62
    ],
    "reserved_universe": 63,
    "config_index": 15,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "centre",
    "camera_visible_px": 79,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-17",
   "name": "Rope 17",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "broken",
   "verified": "confirmed",
   "summary": "Never lights (box 2 OUT7); swap test deferred, carrying on with 18 ropes.",
   "specs": {
    "box": 2,
    "out": 7,
    "universes": [
     64,
     65,
     66
    ],
    "reserved_universe": 67,
    "config_index": 16,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "not seen (dark)",
    "camera_visible_px": null,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap",
    "plan"
   ]
  },
  {
   "id": "rope-18",
   "name": "Rope 18",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT8, universes 68-70.",
   "specs": {
    "box": 2,
    "out": 8,
    "universes": [
     68,
     69,
     70
    ],
    "reserved_universe": 71,
    "config_index": 17,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "left-centre",
    "camera_visible_px": 94,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-19",
   "name": "Rope 19",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT9, universes 72-74.",
   "specs": {
    "box": 2,
    "out": 9,
    "universes": [
     72,
     73,
     74
    ],
    "reserved_universe": 75,
    "config_index": 18,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "far left",
    "camera_visible_px": 47,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "rope-20",
   "name": "Rope 20",
   "kind": "rope",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "working",
   "verified": "confirmed",
   "summary": "10 m LED rope on box 2 OUT10, universes 76-78.",
   "specs": {
    "box": 2,
    "out": 10,
    "universes": [
     76,
     77,
     78
    ],
    "reserved_universe": 79,
    "config_index": 19,
    "pixels": 400,
    "pixels_per_universe": "170 + 170 + 60",
    "pile_at_agape": "far left",
    "camera_visible_px": 66,
    "model": "AoShow 360 LED Flex Neon, 24 V, UCS1903, 40 px/m, 150 W full white",
    "colour_order": "RGB (confirmed 18 Oct)",
    "plug_end": "male round plug is the input; direction along the coil not resolved yet"
   },
   "parent": "psac10-2",
   "sources": [
    "rope-config",
    "rope-map",
    "pixelmap"
   ]
  },
  {
   "id": "cam-gallery",
   "name": "Gallery cam",
   "kind": "camera",
   "place": {
    "now": "agape",
    "show": "grayarea"
   },
   "owner": "will",
   "status": "working",
   "verified": "confirmed",
   "summary": "Reolink E1 Pro (indoor, pan/tilt, mic). At Agape on Agape WiFi, aimed at all coil piles, the HUB75 and the PSAC10s. Used for calibration, snapshots and (planned) the agent's gallery eye.",
   "specs": {
    "model": "Reolink E1 Pro",
    "rtsp": "rtsp://admin:<password>@10.0.7.76:554/Preview_01_main (use -rtsp_transport tcp)",
    "ptz": "HTTPS JSON API https://10.0.7.76/cgi-bin/api.cgi (self-signed): Login -> token -> PtzCtrl (Left/Right/Up/Down/Stop) -> Logout. Always send Stop.",
    "spotlight": "motion spotlight off (Will, 16 Oct)",
    "aim": "18 Oct 13:59; bumped once between 14:11 and 14:33"
   },
   "interfaces": [
    {
     "name": "Wi-Fi",
     "ip": "10.0.7.76",
     "how": "DHCP on Agape WiFi since 18 Oct 14:09 (was 192.168.8.103, then .189, on the GL.iNet)",
     "scenario": "now"
    },
    {
     "name": "Wi-Fi",
     "ip": "unknown",
     "how": "Gray Area gallery network, reached by bitcube; exposed on bitcube's tailnet (planned)",
     "scenario": "show"
    }
   ],
   "accounts": [
    {
     "service": "Reolink",
     "user": "admin",
     "auth": "camera admin password (secrets.local.json -> cameras.gallery)"
    }
   ],
   "ports": [
    {
     "port": 554,
     "proto": "tcp",
     "service": "RTSP"
    },
    {
     "port": 443,
     "proto": "tcp",
     "service": "HTTPS API (PTZ)"
    }
   ],
   "open_questions": [
    "q-gallery-cam-ip",
    "q-gallery-network"
   ],
   "sources": [
    "claude-md",
    "hardware-doc",
    "log"
   ]
  },
  {
   "id": "cam-river",
   "name": "River cam",
   "kind": "camera",
   "place": {
    "now": "river",
    "show": "river"
   },
   "owner": "will",
   "status": "unknown",
   "verified": "confirmed",
   "summary": "Reolink E1 Outdoor Pro behind the GL.iNet router. Left Agape for the river with Colin on the evening of 18 Oct (it used to sit on the rack controller here).",
   "specs": {
    "model": "Reolink E1 Outdoor Pro",
    "rtsp": "rtsp://admin:<password>@192.168.8.217:554/Preview_01_main",
    "ptz": "https://192.168.8.217/cgi-bin/api.cgi (same API as the gallery cam)",
    "serial": "on its base"
   },
   "interfaces": [
    {
     "name": "LAN",
     "ip": "192.168.8.217",
     "how": "behind the GL.iNet (192.168.8.0/24); wired or Wi-Fi unverified"
    }
   ],
   "accounts": [
    {
     "service": "Reolink",
     "user": "admin",
     "auth": "camera admin password (secrets.local.json -> cameras.river)"
    }
   ],
   "ports": [
    {
     "port": 554,
     "proto": "tcp",
     "service": "RTSP"
    },
    {
     "port": 443,
     "proto": "tcp",
     "service": "HTTPS API (PTZ)"
    }
   ],
   "open_questions": [
    "q-river-capture"
   ],
   "sources": [
    "hardware-doc",
    "log",
    "session-a"
   ],
   "notes": [
    "Reachable only while the GL.iNet is online on Will's tailnet."
   ]
  },
  {
   "id": "glinet",
   "name": "GL.iNet router",
   "kind": "router",
   "place": {
    "now": "river",
    "show": "river"
   },
   "owner": "will",
   "status": "unknown",
   "verified": "confirmed",
   "summary": "GL.iNet GL-AXT1800 (Slate AX) travel router running Tailscale: puts the river cam's subnet 192.168.8.0/24 on Will's tailnet. Left Agape for the river with Colin and the river cam on the evening of 18 Oct; offline at 17:50.",
   "specs": {
    "model": "GL.iNet GL-AXT1800 (Slate AX)",
    "tailscale_routes": [
     "192.168.8.0/24 (approved)"
    ],
    "os": "OpenWrt-based (GL.iNet firmware)",
    "route_history": "Also advertised 10.0.7.0/24 (the Agape LAN) until 18 Oct 17:47, when Gavin un-approved it: bitcube accepts routes, so once the router left, bitcube lost the gallery cam and the eero."
   },
   "interfaces": [
    {
     "name": "WAN",
     "ip": null,
     "how": "at the river: unknown (Starlink?); was 10.0.7.150 on the Agape LAN until 18 Oct",
     "verified": "unverified"
    },
    {
     "name": "LAN",
     "ip": "192.168.8.1/24",
     "how": "camera subnet"
    },
    {
     "name": "Tailscale",
     "ip": "100.124.152.62",
     "ipv6": "fd7a:115c:a1e0::63b:983f",
     "how": "Will's tailnet, subnet router"
    }
   ],
   "hostnames": [
    "gl-axt1800",
    "gl-axt1800.tail03bc1a.ts.net"
   ],
   "accounts": [
    {
     "service": "Tailscale",
     "user": "itsriverbox@outlook.com"
    },
    {
     "service": "GL.iNet admin",
     "auth": "router admin password (Will)",
     "verified": "unverified"
    }
   ],
   "notes": [
    "Its uplink at the river (Starlink) and power are unverified."
   ],
   "open_questions": [
    "q-river-capture"
   ],
   "sources": [
    "tailscale-bitcube",
    "hardware-doc",
    "session-a"
   ]
  },
  {
   "id": "eero",
   "name": "Agape eero",
   "kind": "router",
   "place": {
    "now": "agape",
    "show": "agape"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "The house router: gateway, DHCP and DNS for Agape WiFi (10.0.7.0/24).",
   "interfaces": [
    {
     "name": "LAN",
     "ip": "10.0.7.1/24",
     "how": "gateway, DHCP, DNS"
    }
   ],
   "notes": [
    "Nothing on it is port-forwarded to the show machines."
   ],
   "sources": [
    "ssh-bitcube"
   ]
  },
  {
   "id": "gallery-net",
   "name": "Gray Area gallery network",
   "kind": "network",
   "place": {
    "now": null,
    "show": "grayarea"
   },
   "owner": "lilian",
   "status": "planned",
   "verified": "unverified",
   "summary": "Gray Area's own Wi-Fi/internet. Planned: bitcube's internet uplink and the gallery cam's network.",
   "notes": [
    "Who to ask for credentials and whether devices can talk to each other on it: open."
   ],
   "open_questions": [
    "q-gallery-network"
   ],
   "owner_verified": "unverified",
   "sources": [
    "plan"
   ],
   "short": "Gallery network"
  },
  {
   "id": "gallery-switch",
   "name": "Lighting switch",
   "kind": "network",
   "place": {
    "now": null,
    "show": "grayarea"
   },
   "owner": "colin",
   "status": "planned",
   "verified": "unverified",
   "summary": "A dumb Ethernet switch on bitcube's lighting NIC (2.0.0.8) for the rack controller and both PSAC10s.",
   "notes": [
    "Colin offered a switch (16 Oct). The 17 Oct notes also put the gallery cam on this switch; the current plan puts it on the gallery Wi-Fi."
   ],
   "open_questions": [
    "q-gallery-network"
   ],
   "sources": [
    "signal",
    "log"
   ]
  },
  {
   "id": "gavin-mini",
   "name": "Gavin's Mac mini",
   "kind": "computer",
   "place": {
    "now": "agape",
    "show": "agape"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "Always-on Mac mini at Agape: session A (the coordinator) and docs sessions like this one. Pulls the repo, can SSH into bitcube.",
   "specs": {
    "model": "Mac mini (M1, 2020)",
    "os": "macOS Tahoe"
   },
   "interfaces": [
    {
     "name": "LAN",
     "ip": "10.0.7.203",
     "how": "Agape"
    },
    {
     "name": "Tailscale",
     "ip": "100.126.113.14",
     "how": "Gavin's tailnet tailddc624 (subnet router for 10.0.7.0/24 there)"
    }
   ],
   "hostnames": [
    "gav-m1-mini",
    "gav-m1-mini.local",
    "gav-m1-mini.tailddc624.ts.net"
   ],
   "accounts": [
    {
     "service": "macOS",
     "user": "gav"
    },
    {
     "service": "GitHub",
     "user": "gavaiken"
    },
    {
     "service": "Tailscale",
     "user": "gav.aiken@gmail.com"
    }
   ],
   "access": [
    {
     "method": "SSH alias",
     "address": "`ssh bitcube` -> user@100.73.68.60",
     "auth": "~/.ssh/id_ed25519"
    }
   ],
   "sources": [
    "tailscale-mini"
   ],
   "short": "Gavin's Mac mini"
  },
  {
   "id": "gavin-air",
   "name": "Gavin's MacBook Air",
   "kind": "computer",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "Gavin's laptop: VNC, RDP (Windows App) and SMB into bitcube over Tailscale. Holds the 7 Oct Chromatik model and TD patch (probably).",
   "interfaces": [
    {
     "name": "Tailscale",
     "ip": "100.92.63.88",
     "how": "Gavin's tailnet"
    },
    {
     "name": "LAN at Agape",
     "ip": "10.0.7.28",
     "how": "seen 18 Oct"
    }
   ],
   "hostnames": [
    "gavins-macbook-air"
   ],
   "accounts": [
    {
     "service": "Tailscale",
     "user": "gav.aiken@gmail.com"
    }
   ],
   "sources": [
    "tailscale-mini",
    "log"
   ],
   "short": "Gavin's Air"
  },
  {
   "id": "andi-pc",
   "name": "Andi's PC (estrogen)",
   "kind": "computer",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "andi",
   "status": "working",
   "verified": "confirmed",
   "summary": "Andi's Windows PC on Will's tailnet: Colorlight configuration and TouchDesigner; RDP/VNC into bitcube.",
   "interfaces": [
    {
     "name": "Tailscale",
     "ip": "100.87.170.119",
     "ipv6": "fd7a:115c:a1e0::a23b:aa78",
     "how": "Will's tailnet"
    }
   ],
   "hostnames": [
    "estrogen",
    "estrogen.tail03bc1a.ts.net"
   ],
   "accounts": [
    {
     "service": "Tailscale",
     "user": "andixdoud@gmail.com"
    }
   ],
   "notes": [
    "Offline when checked (last seen 21 h before 17:16, 18 Oct)."
   ],
   "sources": [
    "tailscale-bitcube"
   ],
   "short": "estrogen (Andi)"
  },
  {
   "id": "smelterplume",
   "name": "smelterplume (Mac)",
   "kind": "computer",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "will",
   "status": "unknown",
   "verified": "unverified",
   "summary": "A macOS device on Will's tailnet, logged in with the project account. Probably Will's laptop (unverified).",
   "interfaces": [
    {
     "name": "Tailscale",
     "ip": "100.75.114.92",
     "ipv6": "fd7a:115c:a1e0::383b:725d",
     "how": "Will's tailnet"
    }
   ],
   "hostnames": [
    "smelterplume",
    "smelterplume.tail03bc1a.ts.net"
   ],
   "accounts": [
    {
     "service": "Tailscale",
     "user": "itsriverbox@outlook.com"
    }
   ],
   "open_questions": [
    "q-smelterplume"
   ],
   "sources": [
    "tailscale-bitcube"
   ],
   "short": "smelterplume"
  },
  {
   "id": "dave-laptop",
   "name": "Dave's computer",
   "kind": "computer",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "dave",
   "status": "unknown",
   "verified": "unverified",
   "summary": "Dave can SSH into bitcube with his key (added 18 Oct). No Dave device was visible on Will's tailnet on 18 Oct.",
   "access": [
    {
     "method": "SSH",
     "address": "user@bitcube.tail03bc1a.ts.net",
     "port": 22,
     "auth": "Dave's SSH key (comment river, SHA256:aXW4LMdJXbPfPv8mRImLDWTjkVZ3f6Y818HEqin+/No)"
    }
   ],
   "accounts": [
    {
     "service": "GitHub",
     "user": "drocamor"
    }
   ],
   "open_questions": [
    "q-dave-tailnet"
   ],
   "sources": [
    "log",
    "tailscale-bitcube"
   ]
  },
  {
   "id": "starlink",
   "name": "Starlink",
   "kind": "router",
   "place": {
    "now": "river",
    "show": "river"
   },
   "owner": "will",
   "status": "planned",
   "verified": "unverified",
   "summary": "Satellite uplink for the river kit. Outbound only (carrier-grade NAT), so devices there must push out, or be reached over Tailscale.",
   "notes": [
    "Deployment date and whether it was picked up: unverified (planned for the 15-19 Oct river trip)."
   ],
   "open_questions": [
    "q-river-capture"
   ],
   "sources": [
    "signal",
    "river-agent"
   ]
  },
  {
   "id": "river-power",
   "name": "Solar and battery",
   "kind": "power",
   "place": {
    "now": "river",
    "show": "river"
   },
   "owner": "will",
   "status": "planned",
   "verified": "unverified",
   "summary": "Off-grid power for the river kit (camera, router, Starlink).",
   "sources": [
    "signal"
   ]
  },
  {
   "id": "public-internet",
   "name": "Public internet",
   "kind": "network",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "team",
   "status": "working",
   "verified": "confirmed",
   "summary": "Everything that leaves a site: Modal, GitHub, Open-Meteo, Tailscale's coordination servers."
  },
  {
   "id": "open-meteo",
   "name": "Open-Meteo",
   "kind": "service",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "team",
   "status": "working",
   "verified": "confirmed",
   "summary": "Free weather API the tick reads for current conditions near the river (no key).",
   "specs": {
    "url": "https://api.open-meteo.com/v1/forecast",
    "place": "near Dobbins, California"
   },
   "sources": [
    "river-agent",
    "modal-live"
   ]
  },
  {
   "id": "tailnet-will",
   "name": "Will's tailnet (tail03bc1a)",
   "kind": "tailnet",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "will",
   "status": "working",
   "verified": "confirmed",
   "summary": "The project tailnet: bitcube, the GL.iNet (camera subnet router), Andi's PC, smelterplume. Free plan, max 6 users.",
   "specs": {
    "tailnet": "itsriverbox@outlook.com",
    "magicdns": "tail03bc1a.ts.net",
    "owner_login": "itsriverbox@outlook.com",
    "members_seen": [
     "itsriverbox@outlook.com",
     "andixdoud@gmail.com",
     "gav.aiken@gmail.com (Admin; his devices stay on his own tailnet)"
    ],
    "subnet_routes": [
     "192.168.8.0/24 via gl-axt1800 (river cam)",
     "10.0.7.76/32 via bitcube (gallery cam)"
    ]
   },
   "notes": [
    "Node sharing doesn't carry subnet routes: to reach either camera through a route, a device must be in this tailnet itself (on Gavin's devices, use the account switcher)."
   ],
   "sources": [
    "tailscale-bitcube",
    "log"
   ],
   "short": "Will's tailnet"
  },
  {
   "id": "tailnet-gavin",
   "name": "Gavin's tailnet (tailddc624)",
   "kind": "tailnet",
   "place": {
    "now": "internet",
    "show": "internet"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "Gavin's own tailnet: the Mac mini (subnet router for Agape 10.0.7.0/24) and the Air. bitcube is node-shared into it as 100.73.68.60.",
   "specs": {
    "magicdns": "tailddc624.ts.net",
    "owner_login": "gav.aiken@gmail.com"
   },
   "notes": [
    "Don't move the mini to Will's tailnet: it's Gavin's remote-access path."
   ],
   "sources": [
    "tailscale-mini",
    "log"
   ],
   "short": "Gavin's tailnet"
  },
  {
   "id": "modal-app",
   "name": "river-agent app",
   "kind": "cloud-app",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "Dave's Modal app: a cron tick (one agent step every 5 min), a watch function (one per camera clip), and the web function serving the page, /data and /ingest.",
   "specs": {
    "workspace": "to-be-a-river",
    "app": "river-agent",
    "web_label": "river",
    "base_url": "https://to-be-a-river--river.modal.run",
    "tick": "cron */5 9-23,0-1 * * * (America/Chicago), max 1 container, runs orchestrator.run_once()",
    "watch": "spawned by /ingest for each mp4 clip; asks Clef-Omni the agent's questions; writes reflex.json",
    "web": "FastAPI (ASGI), scales to zero, up to 50 concurrent requests",
    "model": "deepseek-ai/DeepSeek-V4-Flash",
    "deploy": "GitHub Actions on push to main of drocamor/river-agent",
    "secrets_by_name": [
     "river-agent-web (INGEST_TOKEN, SITE_TOKEN)",
     "river-agent-api-key (VLLM_API_KEY)",
     "river-agent-proxy (MODAL_PROXY_KEY, MODAL_PROXY_SECRET)"
    ],
    "live_18_oct": "tick 3356, mood expansive, scene flow / dry-bones, lifetime errors 0"
   },
   "sources": [
    "river-agent",
    "modal-live"
   ]
  },
  {
   "id": "ep-data",
   "name": "GET /data/{file}",
   "kind": "endpoint",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "The live files: status.json, state.json, pad.json, journal_recent.json, reflex.json (+ scenes.jsonl, journal.jsonl). bitcube's mirror will pull these every 3 s.",
   "specs": {
    "url": "https://to-be-a-river--river.modal.run/data/state.json",
    "auth": "none today (SITE_TOKEN not set); with SITE_TOKEN: cookie river_token",
    "cache": "no-store"
   },
   "sources": [
    "river-agent",
    "modal-live"
   ],
   "short": "GET /data/{file}"
  },
  {
   "id": "ep-ingest",
   "name": "POST /ingest/{location}/{channel}",
   "kind": "endpoint",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "Where sensors push media: Authorization: Bearer <INGEST_TOKEN>. An mp4 to channel video starts the watch loop.",
   "specs": {
    "url": "https://to-be-a-river--river.modal.run/ingest/gallery/video",
    "auth": "Authorization: Bearer <INGEST_TOKEN> (or x-ingest-token header)",
    "max_body": "25 MB"
   },
   "sources": [
    "river-agent"
   ],
   "short": "POST /ingest/…"
  },
  {
   "id": "ep-canvas",
   "name": "GET /canvas.html",
   "kind": "endpoint",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "The sculpture alone, full window, for TouchDesigner's Web Render TOP. ?w=&h= sets the design area (default 480x270).",
   "specs": {
    "url": "https://to-be-a-river--river.modal.run/canvas.html?w=480&h=270",
    "polls": "status.json every 3 s, state.json on change (or every 30 s), reflex.json every 2 s"
   },
   "sources": [
    "river-agent",
    "modal-live"
   ]
  },
  {
   "id": "ep-page",
   "name": "GET / (journal page)",
   "kind": "endpoint",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "The installation's page: the sculpture, a status strip and the agent's journal. Kiosk mode ?kiosk=1. /healthz answers {\"ok\": true}.",
   "specs": {
    "url": "https://to-be-a-river--river.modal.run/",
    "healthz": "https://to-be-a-river--river.modal.run/healthz"
   },
   "sources": [
    "river-agent",
    "modal-live"
   ],
   "short": "GET / (page)"
  },
  {
   "id": "modal-volume",
   "name": "Volume river-agent-data",
   "kind": "storage",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "The agent's state: journal.jsonl, status.json, state.json, scenes.jsonl, pad.json, journal_recent.json, reflex.json, media/<location>/<channel>/<time>.<ext>.",
   "specs": {
    "retention": "media 72 h; video clips 1 h; gallery frames 1 h (visitors)"
   },
   "sources": [
    "river-agent"
   ],
   "short": "river-agent-data"
  },
  {
   "id": "modal-vllm",
   "name": "DeepSeek-V4-Flash (vLLM)",
   "kind": "cloud-app",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "The agent's LLM: Modal app river-agent-vllm-deepseek-v4-flash, OpenAI-compatible, behind Modal proxy auth.",
   "specs": {
    "url": "https://to-be-a-river--river-agent-vllm-deepseek-v4-flash-serve.modal.run",
    "deploy": "by hand (models.yml workflow)"
   },
   "sources": [
    "river-agent"
   ],
   "short": "DeepSeek-V4-Flash"
  },
  {
   "id": "modal-perception",
   "name": "Perception (vision)",
   "kind": "cloud-app",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "Modal app river-agent-perception: turns camera frames into text for the tick. Audio perception is off.",
   "specs": {
    "vision_url": "https://to-be-a-river--river-agent-perception-vision.modal.run",
    "audio_url": "https://to-be-a-river--river-agent-perception-audio.modal.run (disabled)"
   },
   "sources": [
    "river-agent"
   ],
   "short": "Perception"
  },
  {
   "id": "modal-clef",
   "name": "Clef-Omni",
   "kind": "cloud-app",
   "place": {
    "now": "modal",
    "show": "modal"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "Modal app river-agent-clef: a decision model that answers the agent's typed questions about each 10 s clip (fast perception). Asleep until clips arrive.",
   "specs": {
    "app": "river-agent-clef",
    "class": "ClefOmni",
    "active_hours": "9-2 (America/Chicago)"
   },
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "repo-river-bitcube",
   "name": "gavaiken/river-bitcube",
   "kind": "repo",
   "place": {
    "now": "github",
    "show": "github"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "This repo (public): docs (source of truth), TouchDesigner files and build scripts, tools. Checked out on bitcube and on Gavin's mini.",
   "specs": {
    "url": "https://github.com/gavaiken/river-bitcube",
    "visibility": "public",
    "branches": [
     "main",
     "snapshot (orphan, force-pushed)"
    ],
    "checkouts": [
     "bitcube: C:\\Users\\user\\River\\river-bitcube",
     "mini: ~/claude/Source/river-bitcube"
    ]
   },
   "sources": [
    "log"
   ],
   "short": "river-bitcube"
  },
  {
   "id": "repo-river-agent",
   "name": "drocamor/river-agent",
   "kind": "repo",
   "place": {
    "now": "github",
    "show": "github"
   },
   "owner": "dave",
   "status": "working",
   "verified": "confirmed",
   "summary": "Dave's repo (private): the agent, the page, the Modal app. Merges to main deploy to Modal. gavaiken has push access.",
   "specs": {
    "url": "https://github.com/drocamor/river-agent",
    "visibility": "private",
    "ci": "deploy.yml (push to main deploys modal_deploy/app.py), models.yml (manual model deploys)",
    "actions_secrets_by_name": [
     "MODAL_TOKEN_ID",
     "MODAL_TOKEN_SECRET"
    ]
   },
   "sources": [
    "river-agent",
    "log"
   ],
   "short": "river-agent"
  },
  {
   "id": "gh-pages",
   "name": "Status site (GitHub Pages)",
   "kind": "website",
   "place": {
    "now": "github",
    "show": "github"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "Public status page and these docs, built from main:/docs.",
   "specs": {
    "url": "https://gavaiken.github.io/river-bitcube/",
    "system_docs": "https://gavaiken.github.io/river-bitcube/system/",
    "llms_txt": "https://gavaiken.github.io/river-bitcube/llms.txt"
   },
   "sources": [
    "log"
   ],
   "short": "Status site (Pages)"
  },
  {
   "id": "gh-snapshot",
   "name": "snapshot branch",
   "kind": "repo",
   "place": {
    "now": "github",
    "show": "github"
   },
   "owner": "gavin",
   "status": "working",
   "verified": "confirmed",
   "summary": "Orphan branch holding one gallery.jpg, replaced every 15 minutes (08:00-02:00) by bitcube. The status page shows it.",
   "specs": {
    "url": "https://raw.githubusercontent.com/gavaiken/river-bitcube/snapshot/gallery.jpg",
    "size": "640x360 JPEG, about 60 KB"
   },
   "sources": [
    "log"
   ]
  }
 ],
 "links": [
  {
   "id": "artnet-box1",
   "from": "bitcube",
   "to": "psac10-1",
   "medium": "artnet",
   "protocol": "Art-Net ArtDmx, UDP 6454",
   "summary": "TouchDesigner sends ropes 1-10: universes 0-39 from 2.0.0.8 to 2.0.0.10.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "80 universes in total (this box 0-39), 170 RGB px per universe, 4 per rope (3 used, 4th black)",
   "rate": "40 fps target, about 35 sent",
   "auth": "none (isolated lighting network)",
   "path": "bitcube Ethernet (2.0.0.8/8) -> rack/box cabling (exact cable path unverified)",
   "sources": [
    "rope-config",
    "log",
    "ssh-bitcube"
   ],
   "label": "Art-Net U0–39"
  },
  {
   "id": "artnet-box2",
   "from": "bitcube",
   "to": "psac10-2",
   "medium": "artnet",
   "protocol": "Art-Net ArtDmx, UDP 6454",
   "summary": "TouchDesigner sends ropes 11-20: universes 40-79 from 2.0.0.8 to 2.0.0.11.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "universes 40-79",
   "rate": "40 fps target, about 35 sent",
   "auth": "none (isolated lighting network)",
   "sources": [
    "rope-config",
    "log"
   ],
   "label": "Art-Net U40–79"
  },
  {
   "id": "net-rack-box1",
   "from": "rack-controller",
   "to": "psac10-1",
   "medium": "ethernet",
   "protocol": "AoShow controller link (Ethernet, NET1 to NET1)",
   "summary": "Rack NET1 to box 1 NET1. The rack plays its SD-card program over this only when Art-Net stops.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "hardware-doc"
   ],
   "label": "NET1"
  },
  {
   "id": "net-box2-feed",
   "from": "rack-controller",
   "to": "psac10-2",
   "medium": "ethernet",
   "protocol": "AoShow controller link (Ethernet)",
   "summary": "Box 2 is fed from rack NET2, or chained from box 1 NET2: not checked.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "unverified",
   "direction": "one-way",
   "open_questions": [
    "q-box2-feed"
   ],
   "sources": [
    "hardware-doc"
   ]
  },
  {
   "id": "lighting-switch",
   "from": "bitcube",
   "to": "gallery-switch",
   "medium": "ethernet",
   "protocol": "Ethernet (2.0.0.8/8 lighting network)",
   "summary": "bitcube's lighting NIC to a dumb switch that feeds the rack controller and both PSAC10s.",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "open_questions": [
    "q-gallery-network"
   ],
   "sources": [
    "log",
    "signal"
   ]
  },
  {
   "id": "switch-rack",
   "from": "gallery-switch",
   "to": "rack-controller",
   "medium": "ethernet",
   "protocol": "Ethernet",
   "summary": "Switch to the rack controller (and on to the PSAC10s).",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "sources": [
    "log"
   ]
  },
  {
   "id": "xlr-rope-01",
   "from": "psac10-1",
   "to": "rope-01",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT1 to rope 1 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 0-2",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-02",
   "from": "psac10-1",
   "to": "rope-02",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT2 to rope 2 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 4-6",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-03",
   "from": "psac10-1",
   "to": "rope-03",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT3 to rope 3 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 8-10",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-04",
   "from": "psac10-1",
   "to": "rope-04",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT4 to rope 4 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 12-14",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-05",
   "from": "psac10-1",
   "to": "rope-05",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT5 to rope 5 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 16-18",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-06",
   "from": "psac10-1",
   "to": "rope-06",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT6 to rope 6 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 20-22",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-07",
   "from": "psac10-1",
   "to": "rope-07",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT7 to rope 7 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 24-26",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-08",
   "from": "psac10-1",
   "to": "rope-08",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT8 to rope 8 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 28-30",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-09",
   "from": "psac10-1",
   "to": "rope-09",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT9 to rope 9 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "broken",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 32-34",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-10",
   "from": "psac10-1",
   "to": "rope-10",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 1 OUT10 to rope 10 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 36-38",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-11",
   "from": "psac10-2",
   "to": "rope-11",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT1 to rope 11 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 40-42",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-12",
   "from": "psac10-2",
   "to": "rope-12",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT2 to rope 12 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 44-46",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-13",
   "from": "psac10-2",
   "to": "rope-13",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT3 to rope 13 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 48-50",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-14",
   "from": "psac10-2",
   "to": "rope-14",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT4 to rope 14 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 52-54",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-15",
   "from": "psac10-2",
   "to": "rope-15",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT5 to rope 15 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 56-58",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-16",
   "from": "psac10-2",
   "to": "rope-16",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT6 to rope 16 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 60-62",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-17",
   "from": "psac10-2",
   "to": "rope-17",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT7 to rope 17 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "broken",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 64-66",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-18",
   "from": "psac10-2",
   "to": "rope-18",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT8 to rope 18 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 68-70",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-19",
   "from": "psac10-2",
   "to": "rope-19",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT9 to rope 19 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 72-74",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "xlr-rope-20",
   "from": "psac10-2",
   "to": "rope-20",
   "medium": "xlr",
   "protocol": "4-pin XLR: 24 V + SPI data (UCS1903)",
   "summary": "Box 2 OUT10 to rope 20 (via the supplied XLR to round-plug adapter).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "400 px RGB from universes 76-78",
   "sources": [
    "hardware-doc",
    "rope-map"
   ]
  },
  {
   "id": "hdmi-led",
   "from": "bitcube",
   "to": "colorlight-sender",
   "medium": "video",
   "protocol": "HDMI 1920x1080 (display LED 1920X1080)",
   "summary": "TouchDesigner's black full-screen window on the LED display; the panel atlas sits at its top-left.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "payload": "atlas 512x128 at (0,0): quad 1 x 0-255, quad 2 x 256-511",
   "rate": "60 fps",
   "sources": [
    "log",
    "ssh-bitcube"
   ],
   "label": "HDMI 1920×1080"
  },
  {
   "id": "colorlight-rx",
   "from": "colorlight-sender",
   "to": "receive-card",
   "medium": "video",
   "protocol": "Colorlight protocol over Ethernet cable",
   "summary": "Sender to the 5A-75B receive card.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "unverified",
   "direction": "one-way",
   "sources": [
    "hardware-doc"
   ],
   "label": "Colorlight"
  },
  {
   "id": "hub75-q1",
   "from": "receive-card",
   "to": "hub75-quad1",
   "medium": "video",
   "protocol": "HUB75 ribbon",
   "summary": "Receive card to quad 1 (four 128x64 panels).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "log"
   ],
   "label": "HUB75"
  },
  {
   "id": "hub75-q2",
   "from": "receive-card",
   "to": "hub75-quad2",
   "medium": "video",
   "protocol": "HUB75 ribbon",
   "summary": "Receive card to quad 2: not connected yet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "blocked",
   "verified": "awaiting:Andi",
   "direction": "one-way",
   "open_questions": [
    "q-quad2"
   ],
   "sources": [
    "plan"
   ]
  },
  {
   "id": "hub75-rest",
   "from": "receive-card",
   "to": "hub75-more",
   "medium": "video",
   "protocol": "HUB75 ribbon",
   "summary": "The other panels, if they join the show.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "direction": "one-way"
  },
  {
   "id": "usbc-m14",
   "from": "bitcube",
   "to": "m14",
   "medium": "video",
   "protocol": "USB-C DisplayPort",
   "summary": "Primary display: desktop, taskbar, the status page in Chrome.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "ssh-bitcube",
    "claude-md"
   ],
   "label": "USB-C"
  },
  {
   "id": "wifi-bitcube",
   "from": "bitcube",
   "to": "eero",
   "medium": "lan",
   "protocol": "Wi-Fi (Agape WiFi, 5 GHz)",
   "summary": "bitcube's internet and house LAN: 10.0.7.79, default route.",
   "scenarios": [
    "now"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "ssh-bitcube"
   ],
   "label": "Wi-Fi"
  },
  {
   "id": "uplink-bitcube-gallery",
   "from": "bitcube",
   "to": "gallery-net",
   "medium": "lan",
   "protocol": "Wi-Fi or Ethernet 2 (gallery network)",
   "summary": "bitcube's internet at Gray Area (dual-NIC plan: lighting on one NIC, internet on the other).",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "open_questions": [
    "q-gallery-network"
   ],
   "sources": [
    "log"
   ]
  },
  {
   "id": "eero-internet",
   "from": "eero",
   "to": "public-internet",
   "medium": "lan",
   "protocol": "ISP uplink",
   "summary": "Agape's internet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "ssh-bitcube"
   ]
  },
  {
   "id": "gallery-internet",
   "from": "gallery-net",
   "to": "public-internet",
   "medium": "lan",
   "protocol": "Gallery ISP uplink",
   "summary": "Gray Area's internet.",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified"
  },
  {
   "id": "glinet-starlink",
   "from": "glinet",
   "to": "starlink",
   "medium": "starlink",
   "protocol": "Ethernet to the Starlink router",
   "summary": "The river kit's uplink: the GL.iNet behind Starlink at the river (unverified).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "unknown",
   "verified": "unverified",
   "open_questions": [
    "q-river-capture"
   ],
   "sources": [
    "signal"
   ]
  },
  {
   "id": "starlink-internet",
   "from": "starlink",
   "to": "public-internet",
   "medium": "starlink",
   "protocol": "Starlink satellite (CGNAT, outbound only)",
   "summary": "The river's internet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "sources": [
    "signal",
    "river-agent"
   ],
   "label": "Starlink"
  },
  {
   "id": "cam-river-lan",
   "from": "cam-river",
   "to": "glinet",
   "medium": "lan",
   "protocol": "LAN 192.168.8.0/24",
   "summary": "River cam at 192.168.8.217 behind the GL.iNet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "hardware-doc",
    "ssh-bitcube"
   ],
   "label": "LAN"
  },
  {
   "id": "cam-gallery-wifi",
   "from": "cam-gallery",
   "to": "eero",
   "medium": "lan",
   "protocol": "Wi-Fi (Agape WiFi, DHCP 10.0.7.76)",
   "summary": "Gallery cam on the house network.",
   "scenarios": [
    "now"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "claude-md"
   ],
   "label": "Wi-Fi"
  },
  {
   "id": "cam-gallery-galnet",
   "from": "cam-gallery",
   "to": "gallery-net",
   "medium": "lan",
   "protocol": "Wi-Fi (gallery network)",
   "summary": "Gallery cam on Gray Area's network.",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "open_questions": [
    "q-gallery-cam-ip",
    "q-gallery-network"
   ]
  },
  {
   "id": "rtsp-gallery",
   "from": "bitcube",
   "to": "cam-gallery",
   "medium": "lan",
   "protocol": "RTSP over TCP 554 (/Preview_01_main)",
   "summary": "Video from the gallery cam: snapshots, calibration (structured light), the status-page snapshot, and (planned) clips for the agent.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way (bitcube pulls)",
   "auth": "camera admin password (secrets.local.json -> cameras.gallery)",
   "user": "admin",
   "rate": "on demand; RiverSnapshot every 15 min",
   "path": "now: Agape WiFi, direct. Teammates on Will's tailnet reach it through bitcube's 10.0.7.76/32 route. show: gallery network (planned)",
   "sources": [
    "claude-md",
    "log"
   ],
   "label": "RTSP"
  },
  {
   "id": "ptz-gallery",
   "from": "bitcube",
   "to": "cam-gallery",
   "medium": "lan",
   "protocol": "HTTPS 443 JSON API /cgi-bin/api.cgi",
   "summary": "Pan/tilt: Login -> token -> PtzCtrl -> Stop -> Logout (tools/cameras/ptz.py).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "camera admin password -> session token",
   "user": "admin",
   "rate": "by hand (about 2,600 px/s of pan; nudge 0.1-0.4 s)",
   "sources": [
    "claude-md"
   ]
  },
  {
   "id": "rtsp-river",
   "from": "bitcube",
   "to": "cam-river",
   "medium": "tailscale",
   "protocol": "RTSP over TCP 554, via Tailscale subnet route 192.168.8.0/24",
   "summary": "Video from the river cam, through Will's tailnet and the GL.iNet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "unknown",
   "verified": "confirmed",
   "direction": "one-way (bitcube pulls)",
   "auth": "camera admin password (secrets.local.json -> cameras.river)",
   "user": "admin",
   "path": "bitcube -> Tailscale (Will's tailnet) -> GL.iNet subnet route 192.168.8.0/24 -> 192.168.8.217, now at the river over Starlink (unverified)",
   "sources": [
    "log",
    "tailscale-bitcube"
   ],
   "label": "RTSP via Tailscale"
  },
  {
   "id": "ptz-river",
   "from": "bitcube",
   "to": "cam-river",
   "medium": "tailscale",
   "protocol": "HTTPS 443 JSON API via Tailscale",
   "summary": "Pan/tilt for the river cam.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "unknown",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "camera admin password -> session token",
   "user": "admin",
   "sources": [
    "hardware-doc"
   ]
  },
  {
   "id": "ts-bitcube",
   "from": "bitcube",
   "to": "tailnet-will",
   "medium": "tailscale",
   "protocol": "WireGuard (UDP 41641)",
   "summary": "bitcube is a member of Will's tailnet (100.73.68.60), advertises the gallery cam (10.0.7.76/32) and accepts routes.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "tailscale-bitcube"
   ]
  },
  {
   "id": "ts-glinet",
   "from": "glinet",
   "to": "tailnet-will",
   "medium": "tailscale",
   "protocol": "WireGuard; subnet router",
   "summary": "The GL.iNet advertises 192.168.8.0/24 (the river cam) into Will's tailnet. Its 10.0.7.0/24 route was un-approved on 18 Oct 17:47.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "unknown",
   "verified": "confirmed",
   "open_questions": [],
   "sources": [
    "tailscale-bitcube",
    "session-a"
   ],
   "label": "routes 192.168.8.0/24"
  },
  {
   "id": "ts-andi",
   "from": "andi-pc",
   "to": "tailnet-will",
   "medium": "tailscale",
   "protocol": "WireGuard",
   "summary": "Andi's PC on Will's tailnet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "tailscale-bitcube"
   ]
  },
  {
   "id": "ts-smelterplume",
   "from": "smelterplume",
   "to": "tailnet-will",
   "medium": "tailscale",
   "protocol": "WireGuard",
   "summary": "smelterplume on Will's tailnet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "tailscale-bitcube"
   ]
  },
  {
   "id": "ts-dave",
   "from": "dave-laptop",
   "to": "tailnet-will",
   "medium": "tailscale",
   "protocol": "WireGuard",
   "summary": "Dave's access to Will's tailnet (approved 16 Oct per Signal; no device seen 18 Oct).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "unknown",
   "verified": "unverified",
   "open_questions": [
    "q-dave-tailnet"
   ],
   "sources": [
    "signal",
    "tailscale-bitcube"
   ]
  },
  {
   "id": "ts-mini",
   "from": "gavin-mini",
   "to": "tailnet-gavin",
   "medium": "tailscale",
   "protocol": "WireGuard",
   "summary": "The mini on Gavin's tailnet (also its subnet router for 10.0.7.0/24).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "tailscale-mini"
   ]
  },
  {
   "id": "ts-air",
   "from": "gavin-air",
   "to": "tailnet-gavin",
   "medium": "tailscale",
   "protocol": "WireGuard",
   "summary": "The Air on Gavin's tailnet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "sources": [
    "tailscale-mini"
   ]
  },
  {
   "id": "ts-share",
   "from": "tailnet-will",
   "to": "tailnet-gavin",
   "medium": "tailscale",
   "protocol": "Tailscale node share",
   "summary": "bitcube is shared from Will's tailnet into Gavin's (accepted 17 Oct). Subnet routes are not shared.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "log"
   ],
   "label": "node share"
  },
  {
   "id": "ssh-mini",
   "from": "gavin-mini",
   "to": "bitcube",
   "medium": "tailscale",
   "protocol": "SSH, TCP 22",
   "summary": "`ssh bitcube` from the mini: docs sessions, reading state, running PowerShell.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "user": "user",
   "auth": "SSH key ~/.ssh/id_ed25519 (public half in administrators_authorized_keys)",
   "sources": [
    "log"
   ],
   "label": "SSH"
  },
  {
   "id": "vnc-air",
   "from": "gavin-air",
   "to": "bitcube",
   "medium": "tailscale",
   "protocol": "VNC, TCP 5900 (macOS Screen Sharing)",
   "summary": "Shows the real console and HDMI output.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "TightVNC password",
   "sources": [
    "log"
   ]
  },
  {
   "id": "rdp-air",
   "from": "gavin-air",
   "to": "bitcube",
   "medium": "tailscale",
   "protocol": "RDP, TCP 3389 (Windows App)",
   "summary": "Remote desktop. Blanks the HUB75 while connected; hand the session back afterwards.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "user": "user",
   "auth": "Windows password",
   "sources": [
    "log"
   ]
  },
  {
   "id": "smb-air",
   "from": "gavin-air",
   "to": "bitcube",
   "medium": "tailscale",
   "protocol": "SMB, TCP 445",
   "summary": "Finder -> smb://100.73.68.60/River.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "user": "user",
   "auth": "Windows password",
   "sources": [
    "log"
   ]
  },
  {
   "id": "remote-andi",
   "from": "andi-pc",
   "to": "bitcube",
   "medium": "tailscale",
   "protocol": "RDP 3389 / VNC 5900",
   "summary": "Andi's remote access to bitcube over Will's tailnet.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "unverified",
   "direction": "one-way",
   "user": "user",
   "auth": "Windows or VNC password",
   "sources": [
    "signal"
   ]
  },
  {
   "id": "ssh-dave",
   "from": "dave-laptop",
   "to": "bitcube",
   "medium": "tailscale",
   "protocol": "SSH, TCP 22",
   "summary": "Dave's SSH access: user@bitcube.tail03bc1a.ts.net.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "unknown",
   "verified": "unverified",
   "direction": "one-way",
   "user": "user",
   "auth": "Dave's SSH key (comment river)",
   "open_questions": [
    "q-dave-tailnet"
   ],
   "sources": [
    "log"
   ]
  },
  {
   "id": "git-bitcube",
   "from": "bitcube",
   "to": "repo-river-bitcube",
   "medium": "git",
   "protocol": "git over SSH (github.com:22)",
   "summary": "Claude on bitcube commits and pushes to main.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "deploy key C:\\Users\\user\\.ssh\\river_bitcube_deploy (core.sshCommand)",
   "sources": [
    "log"
   ]
  },
  {
   "id": "snapshot-push",
   "from": "bitcube",
   "to": "gh-snapshot",
   "medium": "git",
   "protocol": "git over SSH (force-push, one parentless commit)",
   "summary": "RiverSnapshot pushes the latest gallery.jpg.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "deploy key river_bitcube_deploy",
   "rate": "every 15 min, 08:00-02:00 Pacific",
   "sources": [
    "log"
   ],
   "label": "jpg / 15 min"
  },
  {
   "id": "git-mini-bitcube",
   "from": "gavin-mini",
   "to": "repo-river-bitcube",
   "medium": "git",
   "protocol": "git (GitHub, gavaiken)",
   "summary": "Coordinator and docs sessions pull and push.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "two-way",
   "sources": [
    "log"
   ]
  },
  {
   "id": "git-mini-agent",
   "from": "gavin-mini",
   "to": "repo-river-agent",
   "medium": "git",
   "protocol": "git (GitHub, gavaiken, push access)",
   "summary": "Gavin's clone of Dave's repo; bitcube's copy is synced from here.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "two-way",
   "sources": [
    "log"
   ]
  },
  {
   "id": "pages-build",
   "from": "repo-river-bitcube",
   "to": "gh-pages",
   "medium": "git",
   "protocol": "GitHub Pages build (main:/docs, Jekyll)",
   "summary": "Every push to main rebuilds the public site.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "log"
   ],
   "label": "Pages build"
  },
  {
   "id": "status-page",
   "from": "bitcube",
   "to": "gh-pages",
   "medium": "https",
   "protocol": "HTTPS",
   "summary": "At login Chrome opens the status page on the M14 (local docs if offline).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way (bitcube reads)",
   "sources": [
    "log"
   ]
  },
  {
   "id": "actions-deploy",
   "from": "repo-river-agent",
   "to": "modal-app",
   "medium": "git",
   "protocol": "GitHub Actions -> modal deploy",
   "summary": "A push to main deploys the app.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "MODAL_TOKEN_ID / MODAL_TOKEN_SECRET (GitHub Actions secrets)",
   "sources": [
    "river-agent"
   ],
   "label": "deploy on push"
  },
  {
   "id": "sync-pull",
   "from": "bitcube",
   "to": "ep-data",
   "medium": "https",
   "protocol": "HTTPS GET, every 3 s",
   "summary": "gallery/sync.py on bitcube mirrors status.json, state.json, pad.json, journal_recent.json and reflex.json into a local folder and serves them on 127.0.0.1:8770, so the show keeps its last scene when the internet drops.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "planned",
   "verified": "awaiting:Dave",
   "direction": "one-way (bitcube pulls)",
   "auth": "none (SITE_TOKEN not set); with SITE_TOKEN: cookie river_token",
   "rate": "5 files every 3 s (backs off to 6 s after 10 failures)",
   "api": "get-data",
   "open_questions": [
    "q-sync-ok"
   ],
   "sources": [
    "river-agent",
    "modal-live"
   ],
   "label": "GET /data every 3 s"
  },
  {
   "id": "canvas-webrender",
   "from": "bitcube",
   "to": "ep-canvas",
   "medium": "https",
   "protocol": "HTTPS (TouchDesigner Web Render TOP)",
   "summary": "river.toe's Web Render TOP loads canvas.html straight from Modal (500x800). Off in the running rain file; planned to switch to the local mirror on :8770.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "stopped",
   "verified": "confirmed",
   "direction": "one-way (bitcube pulls)",
   "api": "get-canvas",
   "open_questions": [
    "q-canvas-size"
   ],
   "sources": [
    "log"
   ],
   "label": "canvas.html"
  },
  {
   "id": "ingest-gallery",
   "from": "bitcube",
   "to": "ep-ingest",
   "medium": "https",
   "protocol": "HTTPS POST /ingest/gallery/video (video/mp4)",
   "summary": "capture.py on bitcube records 10 s gallery-cam clips (640 px, 10 fps, H.264 + AAC) back to back and posts each one.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "planned",
   "verified": "awaiting:Dave",
   "direction": "one-way (bitcube pushes)",
   "auth": "Authorization: Bearer <INGEST_TOKEN>",
   "rate": "one 10 s clip every 10 s",
   "api": "post-ingest",
   "open_questions": [
    "q-ingest-names",
    "q-site-token"
   ],
   "sources": [
    "river-agent"
   ],
   "label": "POST clips"
  },
  {
   "id": "ingest-river",
   "from": "cam-river",
   "to": "ep-ingest",
   "medium": "https",
   "protocol": "HTTPS POST /ingest/river/video via a capture agent",
   "summary": "River clips reach Modal through a capture agent: bitcube pulling RTSP over Tailscale, or a device at the river. Not decided.",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "direction": "one-way (push)",
   "auth": "Authorization: Bearer <INGEST_TOKEN>",
   "api": "post-ingest",
   "open_questions": [
    "q-river-capture"
   ],
   "sources": [
    "river-agent",
    "signal"
   ],
   "label": "POST clips"
  },
  {
   "id": "web-volume",
   "from": "ep-data",
   "to": "modal-volume",
   "medium": "internal",
   "protocol": "Modal Volume mount",
   "summary": "Served from the Volume (reloaded at most every 2 s).",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "ingest-volume",
   "from": "ep-ingest",
   "to": "modal-volume",
   "medium": "internal",
   "protocol": "Modal Volume mount",
   "summary": "Stores media/<location>/<channel>/<time>.<ext>; an mp4 spawns watch().",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "app-canvas",
   "from": "modal-app",
   "to": "ep-canvas",
   "medium": "internal",
   "protocol": "web function",
   "summary": "Served by the web function from web/.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "app-page",
   "from": "modal-app",
   "to": "ep-page",
   "medium": "internal",
   "protocol": "web function",
   "summary": "Served by the web function.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "app-data",
   "from": "modal-app",
   "to": "ep-data",
   "medium": "internal",
   "protocol": "web function",
   "summary": "Served by the web function.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "app-ingest",
   "from": "modal-app",
   "to": "ep-ingest",
   "medium": "internal",
   "protocol": "web function",
   "summary": "Handled by the web function.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "app-volume",
   "from": "modal-app",
   "to": "modal-volume",
   "medium": "internal",
   "protocol": "Modal Volume mount",
   "summary": "tick and watch read media and write the journal, state, status and reflexes.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "two-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "tick-llm",
   "from": "modal-app",
   "to": "modal-vllm",
   "medium": "https",
   "protocol": "HTTPS /chat/completions (OpenAI-compatible)",
   "summary": "Each tick's reasoning and tool calls.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "VLLM_API_KEY + Modal proxy auth (MODAL_PROXY_KEY / MODAL_PROXY_SECRET)",
   "rate": "one tick every 5 min, 09:00-01:55 Central",
   "sources": [
    "river-agent"
   ],
   "label": "LLM calls"
  },
  {
   "id": "tick-perception",
   "from": "modal-app",
   "to": "modal-perception",
   "medium": "https",
   "protocol": "HTTPS",
   "summary": "Frames to text for the tick.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "VLLM_API_KEY + Modal proxy auth",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "watch-clef",
   "from": "modal-app",
   "to": "modal-clef",
   "medium": "internal",
   "protocol": "Modal Cls.remote() call",
   "summary": "watch() puts each clip to the agent's questions.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "tick-weather",
   "from": "modal-app",
   "to": "open-meteo",
   "medium": "https",
   "protocol": "HTTPS GET",
   "summary": "Current weather near the river, each tick.",
   "scenarios": [
    "now",
    "show"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "auth": "none",
   "sources": [
    "river-agent"
   ]
  },
  {
   "id": "cam-via-bitcube",
   "from": "tailnet-will",
   "to": "cam-gallery",
   "medium": "tailscale",
   "protocol": "subnet route 10.0.7.76/32 via bitcube",
   "summary": "Devices on Will's tailnet reach the gallery cam (RTSP 554, PTZ HTTPS 443) through bitcube, which advertises its address. Not visible from Gavin's tailnet: node sharing doesn't carry routes.",
   "scenarios": [
    "now"
   ],
   "status": "working",
   "verified": "confirmed",
   "direction": "one-way",
   "label": "route 10.0.7.76/32",
   "sources": [
    "session-a"
   ]
  },
  {
   "id": "cam-via-bitcube-show",
   "from": "tailnet-will",
   "to": "cam-gallery",
   "medium": "tailscale",
   "protocol": "subnet route <gallery cam address>/32 via bitcube",
   "summary": "At Gray Area: bitcube advertises the gallery cam's new address the same way (and drops 10.0.7.76/32), so the team and Modal-side tools on the tailnet can reach it.",
   "scenarios": [
    "show"
   ],
   "status": "planned",
   "verified": "unverified",
   "direction": "one-way",
   "label": "route via bitcube",
   "open_questions": [
    "q-gallery-cam-ip"
   ],
   "sources": [
    "session-a"
   ]
  }
 ],
 "api": {
  "summary": "Modal composes, bitcube renders. Every 5 minutes the agent's tick writes a scene (state.json) and status; between ticks, camera reflexes nudge the scene's knobs (reflex.json). bitcube never renders on Modal: it mirrors these small JSON files locally (gallery/sync.py on 127.0.0.1:8770) and draws the scene itself (canvas.html in TouchDesigner's Web Render TOP, sampled onto the ropes and HUB75), so the show keeps its last scene through an outage. In the other direction, bitcube pushes 10 s camera clips to /ingest with a bearer token. Nothing on Modal connects into bitcube or the tailnet.",
  "base_url": "https://to-be-a-river--river.modal.run",
  "checked": "2026-10-18 17:21 PT: /healthz and /data/* answer without a token (SITE_TOKEN is not set). Examples below are live from that check, trimmed.",
  "contract": [
   {
    "id": "c-pull",
    "item": "bitcube pulls /data/state.json, reflex.json and status.json (plus pad.json and journal_recent.json) every 3 s through the gallery/sync.py mirror, served on 127.0.0.1:8770 (8765 is RopeLights).",
    "state": "awaiting:Dave",
    "note": "The code supports it (--port 8770 --every 3). Dave to OK running it on bitcube (open question)."
   },
   {
    "id": "c-files",
    "item": "File names, fields and meanings below.",
    "state": "confirmed",
    "note": "From river-agent a9cbd2c and the live site."
   },
   {
    "id": "c-offline",
    "item": "If Modal or the internet is down, the mirrored files stop changing: the last scene keeps playing; evolution resumes when the link returns.",
    "state": "confirmed"
   },
   {
    "id": "c-site-token",
    "item": "/data and the page need no token today. If Dave sets SITE_TOKEN, sync.py sends it as the river_token cookie (--token).",
    "state": "confirmed",
    "note": "Verified 18 Oct 17:21 PT."
   },
   {
    "id": "c-push",
    "item": "bitcube pushes gallery-cam clips: POST /ingest/gallery/video, Content-Type video/mp4, Authorization: Bearer <INGEST_TOKEN>, 10 s each, back to back (capture.py --video).",
    "state": "awaiting:Dave",
    "note": "Needs INGEST_TOKEN, and Dave's confirmation of location/channel names and cadence."
   },
   {
    "id": "c-river",
    "item": "River clips: POST /ingest/river/video from whichever capture agent reaches the river cam.",
    "state": "awaiting:Dave",
    "note": "Who runs it is open (Will/Dave)."
   },
   {
    "id": "c-canvas",
    "item": "TouchDesigner renders http://127.0.0.1:8770/canvas.html?w=<w>&h=<h> in a Web Render TOP and samples it onto the ropes and HUB75 with the camera pixel map.",
    "state": "awaiting:Dave",
    "note": "Design size (w x h) for 20x400 ropes + panels: open question."
   }
  ],
  "endpoints": [
   {
    "id": "get-data",
    "method": "GET",
    "path": "/data/{file}",
    "node": "ep-data",
    "state": "confirmed",
    "files": [
     "status.json",
     "state.json",
     "pad.json",
     "journal_recent.json",
     "reflex.json",
     "scenes.jsonl",
     "journal.jsonl"
    ],
    "auth": "None today. With SITE_TOKEN set: cookie river_token=<SITE_TOKEN> (set once by opening any page with ?token=...).",
    "responses": {
     "200": "the file, Cache-Control: no-store",
     "401": "unauthorized (only if SITE_TOKEN is set)",
     "404": "not a served file, or not written yet (state.json is never 404: a placeholder scene is served)"
    },
    "used_by": "gallery/sync.py (mirror), canvas.html and the page (polling)"
   },
   {
    "id": "post-ingest",
    "method": "POST",
    "path": "/ingest/{location}/{channel}",
    "node": "ep-ingest",
    "state": "confirmed",
    "auth": "Authorization: Bearer <INGEST_TOKEN> (or header x-ingest-token: <INGEST_TOKEN>)",
    "params": {
     "location": "lowercase name matching ^[a-z0-9][a-z0-9_-]{0,31}$; questions are asked only for gallery and river",
     "channel": "same pattern; video for mp4 clips (fast perception); camera, microphone, hydrophone for frames and WAVs; any other name for JSON readings",
     "ext": "optional ?ext= when the content type is unknown"
    },
    "body": "The file. Content-Type one of image/jpeg, image/png, audio/wav, audio/flac, audio/ogg, audio/mpeg, video/mp4, application/json. Max 25 MB. A JSON reading must be an object with a non-empty summary.",
    "stores": "media/<location>/<channel>/<YYYY-MM-DDTHH-MM-SS-mmmZ>.<ext> on the Volume; video kept 1 h, other media 72 h. An mp4 spawns watch() (one at a time, in order).",
    "responses": {
     "200": {
      "ok": true,
      "stored": "media/gallery/video/2026-10-22T19-00-10-123Z.mp4",
      "bytes": 412345,
      "pruned": 3
     },
     "400": {
      "ok": false,
      "error": "location and channel must be short lowercase names"
     },
     "401": {
      "ok": false,
      "error": "bad token"
     },
     "413": {
      "ok": false,
      "error": "body larger than 26214400 bytes"
     },
     "415": {
      "ok": false,
      "error": "unknown content type 'text/plain'; send image/jpeg, audio/wav, application/json, or ?ext="
     },
     "503": {
      "ok": false,
      "error": "storage busy, retry: ...",
      "headers": "Retry-After: 30"
     }
    },
    "example_curl": "curl -X POST -H \"Authorization: Bearer $INGEST_TOKEN\" -H \"Content-Type: video/mp4\" --data-binary @clip.mp4 https://to-be-a-river--river.modal.run/ingest/gallery/video",
    "reference_client": "river-agent capture/capture.py --site https://to-be-a-river--river.modal.run --token $INGEST_TOKEN --location gallery --video rtsp://admin:<password>@10.0.7.76:554/Preview_01_main",
    "json_reading_example": {
     "source_id": "river_temp_01",
     "source_type": "water_temperature",
     "location": "river",
     "timestamp": "2026-10-18T12:00:00-07:00",
     "summary": "Water is 11.2 C, down 0.3 since the morning.",
     "metrics": {
      "celsius": 11.2
     }
    }
   },
   {
    "id": "get-canvas",
    "method": "GET",
    "path": "/canvas.html?w=&h=",
    "node": "ep-canvas",
    "state": "confirmed",
    "auth": "none (SITE_TOKEN not set)",
    "params": {
     "w": "design width, default 480",
     "h": "design height, default 270 (w x h is an area; the design takes the window's shape)"
    },
    "note": "Draws the scene client-side at the window's device pixels; polls status.json every 3 s, state.json on a new tick (or every 30 s), reflex.json every 2 s. Never blank: falls back to a default scene."
   },
   {
    "id": "get-page",
    "method": "GET",
    "path": "/",
    "node": "ep-page",
    "state": "confirmed",
    "auth": "none (SITE_TOKEN not set)",
    "params": {
     "kiosk": "1 = canvas only",
     "w,h": "render size",
     "hud": "1 = overlay",
     "token": "SITE_TOKEN, once, becomes a cookie"
    },
    "note": "/healthz returns {\"ok\": true, \"app\": \"river-agent\"}; /journal redirects to /#journal."
   }
  ],
  "files": [
   {
    "name": "status.json",
    "written_by": "tick, at the start (in_progress) and end of every tick",
    "poll": "3 s",
    "fields": {
     "tick": "int, tick number",
     "ok": "bool",
     "error": "string or null",
     "started_at / finished_at": "ISO time",
     "*_epoch": "Unix seconds",
     "duration_s": "float",
     "tick_interval_seconds": "300",
     "schedule": "the tick's cron",
     "next_tick_epoch": "when the next tick is due",
     "mood": "one word from the journal",
     "acted": "the tools the agent called this tick",
     "scene": "animation, palette_name, colors, placeholder",
     "pad": "chord and notes",
     "sensors": "per site_channel: present, file, age_s, fresh",
     "in_progress": "null, or set while a tick runs"
    },
    "example": {
     "tick": 3356,
     "mode": "once",
     "host": "modal",
     "model": "deepseek-ai/DeepSeek-V4-Flash",
     "ok": true,
     "error": null,
     "started_at": "2026-10-19T00:20:06+00:00",
     "finished_at": "2026-10-19T00:21:36+00:00",
     "started_at_epoch": 1792369206.6990602,
     "finished_at_epoch": 1792369296.1452744,
     "duration_s": 89.4,
     "llm_seconds": 87.9,
     "tick_interval_seconds": 300.0,
     "schedule": "*/5 9-23,0-1 * * *",
     "next_tick_epoch": 1792369500.0,
     "journal_entries": 3356,
     "mood": "expansive",
     "journal_written": true,
     "acted": [
      {
       "name": "set_visual_scene",
       "animation": "flow",
       "colors": [
        "#442222",
        "#553333",
        "#775544",
        "#997755",
        "#887766"
       ],
       "palette_name": "dry-bones",
       "speed": 0.31,
       "density": 0.5,
       "brightness": 0.44,
       "turbulence": 0.68,
       "transition_seconds": 90.0,
       "placeholder": false
      }
     ],
     "scene": {
      "animation": "flow",
      "palette_name": "dry-bones",
      "colors": [
       "#442222",
       "#553333",
       "#775544",
       "#997755",
       "#887766"
      ],
      "placeholder": false
     },
     "pad": {
      "chord": "IV (D)",
      "notes": [
       "F#3",
       "A3",
       "D4"
      ],
      "colour": "add9",
      "register": "middle",
      "voices": 3,
      "intensity": 0.46
     },
     "sensor_backend": "media",
     "sensors": {
      "river_camera": {
       "present": true,
       "file": "2026-10-15T22-22-51-715Z.jpg",
       "age_s": 266323,
       "fresh": false
      },
      "river_microphone": {
       "present": true,
       "file": "2026-10-15T22-22-51-715Z.wav",
       "age_s": 266323,
       "fresh": false
      },
      "river_hydrophone": {
       "present": false
      },
      "gallery_camera": {
       "present": true,
       "file": "2026-10-15T22-22-55-432Z.jpg",
       "age_s": 266320,
       "fresh": false
      },
      "gallery_microphone": {
       "present": true,
       "file": "2026-10-15T22-22-55-432Z.wav",
       "age_s": 266320,
       "fresh": false
      },
      "river_weather": {
       "present": true,
       "fresh": true,
       "age_s": 0
      }
     },
     "in_progress": null
    }
   },
   {
    "name": "state.json",
    "written_by": "tick, when the agent calls set_visual_scene",
    "poll": "on a new tick (or every 30 s)",
    "fields": {
     "seq": "int, +1 per change (apply only when it changes)",
     "set_at / set_at_epoch": "when it was set (used to ease)",
     "tick": "int",
     "scene.animation": "drift | flow | ripples | aurora | embers | waves | blooms | pulse",
     "scene.colors": "2-6 lowercase #rrggbb",
     "scene.palette_name": "string (<= 60) or null",
     "scene.speed / density / turbulence": "0-1",
     "scene.brightness": "0.3-1",
     "scene.transition_seconds": "5-300 (default 60)",
     "scene.placeholder": "bool",
     "animations": "the allowed animation names"
    },
    "example": {
     "seq": 1542,
     "set_at": "2026-10-18T17:21:35-07:00",
     "set_at_epoch": 1792369295.5599794,
     "tick": 3356,
     "scene": {
      "animation": "flow",
      "colors": [
       "#442222",
       "#553333",
       "#775544",
       "#997755",
       "#887766"
      ],
      "palette_name": "dry-bones",
      "speed": 0.31,
      "density": 0.5,
      "brightness": 0.44,
      "turbulence": 0.68,
      "transition_seconds": 90.0,
      "placeholder": false
     },
     "animations": [
      "drift",
      "flow",
      "ripples",
      "aurora",
      "embers",
      "waves",
      "blooms",
      "pulse"
     ]
    }
   },
   {
    "name": "reflex.json",
    "written_by": "watch, every few seconds while a camera is answering",
    "poll": "2 s",
    "fields": {
     "updated_epoch": "Unix seconds",
     "ttl_seconds": "ignore the file once older than this (default 90)",
     "offsets": "brightness, speed, density, turbulence; each clamped to +-0.25, added to the scene's knobs",
     "active": "which reflexes contributed",
     "sites": "last reading per site (epoch or null)",
     "reflexes_set_by": "agent or default"
    },
    "example": {
     "updated_epoch": 1792103017.19985,
     "updated_at": "2026-10-15T22:23:37+00:00",
     "ttl_seconds": 90.0,
     "offsets": {
      "brightness": 0.0,
      "speed": 0.0,
      "density": 0.0,
      "turbulence": 0.0
     },
     "active": [],
     "sites": {
      "river": 1792103014.1969469,
      "gallery": 1792103017.19985
     },
     "reflexes_set_by": "agent"
    }
   },
   {
    "name": "pad.json",
    "written_by": "tick (play_pad)",
    "poll": "3 s",
    "fields": {
     "seq": "int",
     "key": "e.g. A major",
     "state": "chord, notes (MIDI), colour (plain|seventh|suspended|open|add9), register (low|middle|high), voices, intensity",
     "events": "timed voice moves: expression / on / off at absolute epochs"
    },
    "note": "The installation will probably have no sound.",
    "example": {
     "seq": 1112,
     "set_at": "2026-10-18T17:21:36-07:00",
     "set_at_epoch": 1792369296.0530698,
     "tick": 3356,
     "key": "A major",
     "channel": 10,
     "backend": "schedule",
     "state": {
      "colour": "add9",
      "register": "middle",
      "voices": 3,
      "intensity": 0.46,
      "transition_seconds": 150.0,
      "notes": [
       54,
       57,
       62
      ],
      "degree": 3,
      "chord": "IV (D)",
      "note_names": [
       "F#3",
       "A3",
       "D4"
      ]
     },
     "from_notes": [
      56,
      62
     ],
     "velocity": 71,
     "expression": 69,
     "events": [
      {
       "at": 1792369296.053,
       "type": "expression",
       "value": 69
      },
      {
       "at": 1792369346.053,
       "type": "off",
       "note": 56
      },
      {
       "at": 1792369346.453,
       "type": "on",
       "note": 54,
       "velocity": 71
      }
     ]
    }
   },
   {
    "name": "journal_recent.json",
    "written_by": "tick",
    "poll": "3 s",
    "fields": {
     "count": "total journal entries",
     "shown": "entries here (up to 50)",
     "entries": "oldest first: tick, timestamp, reflection, mood_word, auto_generated, context"
    },
    "example": {
     "count": 3356,
     "shown": 50,
     "generated_at_epoch": 1792369296.2,
     "entries": [
      {
       "tick": 3356,
       "timestamp": "2026-10-18T17:21:35-07:00",
       "reflection": "(the agent's journal text)",
       "mood_word": "expansive",
       "auto_generated": false
      }
     ]
    }
   }
  ],
  "mirror": {
   "script": "river-agent gallery/sync.py",
   "command": "python gallery/sync.py --site https://to-be-a-river--river.modal.run --port 8770 --every 3 --dir gallery_state",
   "serves": "http://127.0.0.1:8770/ (page), /canvas.html, /data/<file>",
   "writes": "atomic (tmp + rename), only when a file changed",
   "on_failure": "keeps the last files; logs the 1st, 10th and every 100th failure; backs off",
   "status": "planned (session D)"
  }
 },
 "secrets": [
  {
   "id": "camera-admin-password",
   "name": "Reolink admin password",
   "unlocks": "Both cameras: RTSP video and the PTZ API (user admin)",
   "lives_in": [
    "bitcube: river-bitcube\\secrets.local.json (untracked) -> cameras.gallery and cameras.river, inside the RTSP URLs",
    "the Reolink app"
   ],
   "holder": "will",
   "used_by": [
    "tools/cam_snap.ps1",
    "tools/snapshot.ps1",
    "tools/pixelmap/capture.py",
    "tools/cameras/ptz.py"
   ]
  },
  {
   "id": "bitcube-windows-password",
   "name": "bitcube Windows password (user `user`)",
   "unlocks": "Console, RDP, SMB",
   "lives_in": [
    "Will",
    "bitcube: LSA secret (Sysinternals Autologon) for auto-login"
   ],
   "holder": "will",
   "note": "SSH uses keys, not this password."
  },
  {
   "id": "vnc-password",
   "name": "TightVNC password",
   "unlocks": "VNC on bitcube :5900",
   "lives_in": [
    "TightVNC server settings on bitcube"
   ],
   "holder": "unverified (set 17 Oct by Gavin/Andi)"
  },
  {
   "id": "ingest-token",
   "name": "INGEST_TOKEN",
   "unlocks": "POST /ingest on Modal",
   "lives_in": [
    "Modal secret river-agent-web",
    "bitcube: secrets.local.json once Dave shares it (session E)"
   ],
   "holder": "dave"
  },
  {
   "id": "site-token",
   "name": "SITE_TOKEN",
   "unlocks": "The page and /data when set",
   "lives_in": [
    "Modal secret river-agent-web (optional)"
   ],
   "holder": "dave",
   "note": "Not set (verified 18 Oct 17:21 PT)."
  },
  {
   "id": "vllm-api-key",
   "name": "VLLM_API_KEY",
   "unlocks": "The model endpoints",
   "lives_in": [
    "Modal secret river-agent-api-key"
   ],
   "holder": "dave"
  },
  {
   "id": "modal-proxy",
   "name": "MODAL_PROXY_KEY / MODAL_PROXY_SECRET",
   "unlocks": "Modal proxy auth in front of the model endpoints",
   "lives_in": [
    "Modal secret river-agent-proxy"
   ],
   "holder": "dave"
  },
  {
   "id": "modal-deploy-token",
   "name": "MODAL_TOKEN_ID / MODAL_TOKEN_SECRET",
   "unlocks": "Deploying to the Modal workspace",
   "lives_in": [
    "GitHub Actions secrets on drocamor/river-agent"
   ],
   "holder": "dave"
  },
  {
   "id": "deploy-key",
   "name": "river-bitcube deploy key",
   "unlocks": "Push to gavaiken/river-bitcube only",
   "lives_in": [
    "bitcube: C:\\Users\\user\\.ssh\\river_bitcube_deploy (private half)",
    "GitHub repo Settings -> Deploy keys (public half)"
   ],
   "holder": "gavin",
   "note": "Revoke under the repo's Deploy keys."
  },
  {
   "id": "ssh-keys",
   "name": "SSH keys into bitcube",
   "unlocks": "SSH as `user` (admin)",
   "lives_in": [
    "Gavin's mini ~/.ssh/id_ed25519",
    "Dave's computer (key comment river)",
    "public halves: C:\\ProgramData\\ssh\\administrators_authorized_keys"
   ],
   "holder": "gavin, dave"
  },
  {
   "id": "tailscale-account",
   "name": "Project Tailscale account itsriverbox@outlook.com",
   "unlocks": "Will's tailnet admin console",
   "lives_in": [
    "Will"
   ],
   "holder": "will",
   "note": "bitcube's node key expires 2027-04-11."
  },
  {
   "id": "glinet-admin",
   "name": "GL.iNet admin password",
   "unlocks": "The camera router's admin UI",
   "lives_in": [
    "Will"
   ],
   "holder": "will",
   "verified": "unverified"
  },
  {
   "id": "teamviewer",
   "name": "TeamViewer credentials",
   "unlocks": "TeamViewer remote access to bitcube",
   "lives_in": [
    "unknown (2023 BitCube setup)"
   ],
   "holder": "unverified"
  }
 ],
 "people": [
  {
   "id": "colin",
   "name": "Colin",
   "role": "Artist; lead and organiser, ordering. Also: TouchDesigner as the single renderer, per-rope sample paths"
  },
  {
   "id": "justine",
   "name": "Justine Sun Dela Cruz",
   "role": "Meeting notes, visual references, coordination"
  },
  {
   "id": "will",
   "name": "Will Gardiner",
   "role": "Networking, cameras, Tailscale (runs the project tailnet), Windows box, RopeLights app",
   "accounts": [
    {
     "service": "Tailscale",
     "login": "itsriverbox@outlook.com",
     "note": "Project account Will created; owns tailnet tail03bc1a (shows as \"River B\")"
    }
   ]
  },
  {
   "id": "dave",
   "name": "Dave",
   "role": "The agent and its generated visuals on Modal; owns river-agent",
   "accounts": [
    {
     "service": "GitHub",
     "login": "drocamor"
    },
    {
     "service": "Modal",
     "login": "workspace to-be-a-river",
     "note": "Workspace owner unverified"
    }
   ]
  },
  {
   "id": "andi",
   "name": "Andi Doud",
   "role": "HUB75 panels, Colorlight config, TouchDesigner",
   "accounts": [
    {
     "service": "Tailscale",
     "login": "andixdoud@gmail.com",
     "note": "Member of Will's tailnet"
    }
   ]
  },
  {
   "id": "lilian",
   "name": "Lilian (Lily)",
   "role": "Rigging; Gray Area liaison"
  },
  {
   "id": "gavin",
   "name": "Gavin Aiken",
   "role": "Pixel mapping, TouchDesigner harness, this repo and its Claude sessions",
   "accounts": [
    {
     "service": "GitHub",
     "login": "gavaiken"
    },
    {
     "service": "Tailscale",
     "login": "gav.aiken@gmail.com",
     "note": "Own tailnet tailddc624; Admin on Will's tailnet; bitcube is shared into his tailnet"
    }
   ]
  }
 ],
 "open_questions": [
  {
   "id": "q-site-token",
   "ask": "dave",
   "question": "Can bitcube get an INGEST_TOKEN? (SITE_TOKEN: answered, it is not set.)",
   "in_plan": true
  },
  {
   "id": "q-ingest-names",
   "ask": "dave",
   "question": "Which location/channel names and clip cadence should bitcube use for camera ingest? (Code: locations gallery/river, channel video, 10 s clips.)",
   "in_plan": true
  },
  {
   "id": "q-sync-ok",
   "ask": "dave",
   "question": "OK to run gallery/sync.py on bitcube (port 8770)?",
   "in_plan": true
  },
  {
   "id": "q-canvas-size",
   "ask": "dave",
   "question": "Preferred canvas design size (?w=&h=) for 20x400 ropes + panels?",
   "in_plan": true
  },
  {
   "id": "q-oct15-ingest",
   "ask": "dave",
   "question": "What posted gallery and river camera/mic files to /ingest until 15 Oct 22:22 UTC? Is anything meant to be pushing now?",
   "in_plan": false
  },
  {
   "id": "q-dave-tailnet",
   "ask": "dave",
   "question": "Are you on Will's tailnet? No device of yours was visible from bitcube on 18 Oct.",
   "in_plan": false
  },
  {
   "id": "q-quad2",
   "ask": "andi",
   "question": "Receive-card layout for quad 2? Thread size for the panel mounts?",
   "in_plan": true
  },
  {
   "id": "q-ropelights",
   "ask": "will",
   "question": "Retire RopeLights or keep it as a backup? OK for Claude to edit config.local.json?",
   "in_plan": true
  },
  {
   "id": "q-gallery-network",
   "ask": "will",
   "question": "Gallery network: the dual-NIC plan. (Reaching the gallery cam over Tailscale: bitcube advertises its address as a /32, as at Agape since 18 Oct.)",
   "in_plan": true
  },
  {
   "id": "q-gallery-cam-ip",
   "ask": "will",
   "question": "Give the gallery cam a fixed address? At Gray Area, gallery Wi-Fi or back on the GL.iNet?",
   "in_plan": true
  },
  {
   "id": "q-river-capture",
   "ask": "will",
   "question": "At the river: is the GL.iNet on Starlink, and what runs the capture agent that pushes river clips to /ingest (bitcube over Tailscale via 192.168.8.0/24, or a device there)?",
   "in_plan": false
  },
  {
   "id": "q-box2-feed",
   "ask": "will",
   "question": "Is PSAC10 box 2 fed from rack NET2 or chained from box 1 NET2?",
   "in_plan": false
  },
  {
   "id": "q-smelterplume",
   "ask": "will",
   "question": "Whose Mac is smelterplume?",
   "in_plan": false
  },
  {
   "id": "q-fallback",
   "ask": "team",
   "question": "What should the gallery show if bitcube goes down? (Today: the rack controller's SD-card colour cycle.)",
   "in_plan": true
  },
  {
   "id": "q-td-licence",
   "ask": "gavin",
   "question": "Does the show need a TouchDesigner Commercial licence? (bitcube has Non-Commercial.)",
   "in_plan": true
  }
 ],
 "sources": [
  {
   "id": "ssh-bitcube",
   "what": "Read-only PowerShell over `ssh bitcube`: OS, NICs, IPs, routes, monitors, listening ports, services, scheduled tasks, SMB shares, firewall rules, ping of cameras and PSAC10s",
   "when": "2026-10-18 17:15 PT",
   "by": "session I"
  },
  {
   "id": "tailscale-bitcube",
   "what": "`tailscale status` and `tailscale status --json` on bitcube (Will's tailnet view)",
   "when": "2026-10-18 17:16 PT",
   "by": "session I"
  },
  {
   "id": "tailscale-mini",
   "what": "`tailscale status` on Gavin's Mac mini (Gavin's tailnet view)",
   "when": "2026-10-18 17:17 PT",
   "by": "session I"
  },
  {
   "id": "rope-config",
   "what": "Will's C:\\Users\\user\\RopeLights\\config.local.json on bitcube (contains no secrets)",
   "when": "2026-10-18 17:16 PT",
   "by": "session I"
  },
  {
   "id": "modal-live",
   "what": "Unauthenticated GET of https://to-be-a-river--river.modal.run/healthz and /data/{status,state,reflex,pad}.json (all 200, so SITE_TOKEN is not set)",
   "when": "2026-10-18 17:21 PT",
   "by": "session I"
  },
  {
   "id": "river-agent",
   "what": "drocamor/river-agent at a9cbd2c: modal_deploy/app.py, modal_deploy/agent.env, gallery/sync.py, capture/capture.py, tools/site.py, web/canvas.js, tools/visual_scene.py, tools/fast_perception.py, tools/midi_pad.py, .github/workflows",
   "when": "2026-10-18",
   "by": "session I"
  },
  {
   "id": "log",
   "what": "docs/a-river-becoming-log.md (session history)",
   "url": "https://gavaiken.github.io/river-bitcube/a-river-becoming-log.html"
  },
  {
   "id": "hardware-doc",
   "what": "docs/a-river-becoming-hardware.md (verified hardware)",
   "url": "https://gavaiken.github.io/river-bitcube/a-river-becoming-hardware.html"
  },
  {
   "id": "claude-md",
   "what": "CLAUDE.md (facts and gotchas for bitcube)",
   "url": "https://github.com/gavaiken/river-bitcube/blob/main/CLAUDE.md"
  },
  {
   "id": "plan",
   "what": "docs/plan.md (checklist, decisions, open questions)",
   "url": "https://gavaiken.github.io/river-bitcube/plan.html"
  },
  {
   "id": "rope-map",
   "what": "Camera rope identification, 18 Oct 14:11 (docs/img/rope-map-2026-10-18.png)",
   "url": "https://gavaiken.github.io/river-bitcube/img/rope-map-2026-10-18.png"
  },
  {
   "id": "pixelmap",
   "what": "tools/pixelmap/pixelmap-2026-10-18.json (structured-light map, session B)"
  },
  {
   "id": "signal",
   "what": "The team's Signal thread (exported to 17 Oct on Gavin's mini; not public)"
  },
  {
   "id": "session-a",
   "what": "Session A notes, 18 Oct 17:50 and 17:55 (docs/sessions/A.md): GL.iNet to the river, route changes, gallery cam via bitcube, HUB75 gain",
   "url": "https://gavaiken.github.io/river-bitcube/sessions/A.html"
  }
 ],
 "layout": {
  "viewbox": [
   0,
   0,
   1600,
   860
  ],
  "node_size": [
   150,
   46
  ],
  "rope_size": [
   56,
   24
  ],
  "scenarios": {
   "now": {
    "places": {
     "github": [
      20,
      20,
      360,
      250
     ],
     "internet": [
      400,
      20,
      600,
      250
     ],
     "modal": [
      1020,
      20,
      560,
      290
     ],
     "agape": [
      20,
      290,
      1180,
      560
     ],
     "river": [
      1220,
      330,
      360,
      260
     ]
    },
    "nodes": {
     "repo-river-bitcube": [
      110,
      85
     ],
     "repo-river-agent": [
      290,
      85
     ],
     "gh-pages": [
      110,
      165
     ],
     "gh-snapshot": [
      290,
      165
     ],
     "public-internet": [
      520,
      85
     ],
     "open-meteo": [
      700,
      85
     ],
     "dave-laptop": [
      880,
      85
     ],
     "tailnet-will": [
      520,
      160
     ],
     "tailnet-gavin": [
      700,
      160
     ],
     "gavin-air": [
      880,
      160
     ],
     "andi-pc": [
      520,
      235
     ],
     "smelterplume": [
      700,
      235
     ],
     "ep-page": [
      1115,
      85
     ],
     "ep-canvas": [
      1300,
      85
     ],
     "ep-data": [
      1485,
      85
     ],
     "ep-ingest": [
      1115,
      165
     ],
     "modal-app": [
      1300,
      165
     ],
     "modal-volume": [
      1485,
      165
     ],
     "modal-vllm": [
      1115,
      245
     ],
     "modal-perception": [
      1300,
      245
     ],
     "modal-clef": [
      1485,
      245
     ],
     "bitcube": [
      380,
      470
     ],
     "m14": [
      110,
      470
     ],
     "colorlight-sender": [
      650,
      470
     ],
     "receive-card": [
      850,
      470
     ],
     "hub75-quad1": [
      700,
      565
     ],
     "hub75-quad2": [
      880,
      565
     ],
     "hub75-more": [
      1070,
      565
     ],
     "rack-controller": [
      110,
      680
     ],
     "psac10-1": [
      380,
      680
     ],
     "psac10-2": [
      800,
      680
     ],
     "eero": [
      110,
      360
     ],
     "cam-gallery": [
      560,
      360
     ],
     "gavin-mini": [
      1080,
      360
     ],
     "starlink": [
      1310,
      400
     ],
     "river-power": [
      1490,
      400
     ],
     "glinet": [
      1310,
      480
     ],
     "cam-river": [
      1490,
      480
     ]
    }
   },
   "show": {
    "places": {
     "github": [
      20,
      20,
      360,
      250
     ],
     "internet": [
      400,
      20,
      600,
      250
     ],
     "modal": [
      1020,
      20,
      560,
      290
     ],
     "grayarea": [
      20,
      290,
      1180,
      560
     ],
     "river": [
      1220,
      330,
      360,
      260
     ],
     "agape": [
      1220,
      720,
      360,
      120
     ]
    },
    "nodes": {
     "repo-river-bitcube": [
      110,
      85
     ],
     "repo-river-agent": [
      290,
      85
     ],
     "gh-pages": [
      110,
      165
     ],
     "gh-snapshot": [
      290,
      165
     ],
     "public-internet": [
      520,
      85
     ],
     "open-meteo": [
      700,
      85
     ],
     "dave-laptop": [
      880,
      85
     ],
     "tailnet-will": [
      520,
      160
     ],
     "tailnet-gavin": [
      700,
      160
     ],
     "gavin-air": [
      880,
      160
     ],
     "andi-pc": [
      520,
      235
     ],
     "smelterplume": [
      700,
      235
     ],
     "ep-page": [
      1115,
      85
     ],
     "ep-canvas": [
      1300,
      85
     ],
     "ep-data": [
      1485,
      85
     ],
     "ep-ingest": [
      1115,
      165
     ],
     "modal-app": [
      1300,
      165
     ],
     "modal-volume": [
      1485,
      165
     ],
     "modal-vllm": [
      1115,
      245
     ],
     "modal-perception": [
      1300,
      245
     ],
     "modal-clef": [
      1485,
      245
     ],
     "bitcube": [
      380,
      470
     ],
     "m14": [
      110,
      470
     ],
     "colorlight-sender": [
      650,
      470
     ],
     "receive-card": [
      850,
      470
     ],
     "hub75-quad1": [
      700,
      565
     ],
     "hub75-quad2": [
      880,
      565
     ],
     "hub75-more": [
      1070,
      565
     ],
     "rack-controller": [
      110,
      680
     ],
     "psac10-1": [
      380,
      680
     ],
     "psac10-2": [
      800,
      680
     ],
     "gallery-net": [
      110,
      360
     ],
     "gallery-switch": [
      290,
      360
     ],
     "cam-gallery": [
      650,
      360
     ],
     "starlink": [
      1310,
      400
     ],
     "river-power": [
      1490,
      400
     ],
     "glinet": [
      1310,
      480
     ],
     "cam-river": [
      1490,
      480
     ],
     "gavin-mini": [
      1310,
      790
     ],
     "eero": [
      1490,
      790
     ]
    }
   }
  },
  "ropes": "Rope chips are drawn automatically in two rows of five under their PSAC10 (rope_size), so they have no positions here."
 }
}
