Initial SyncGames tree: agent, Android, deploy, docs.

Session-gated MinIO save sync with AppImage GUI, CLI edit/session flow, and Gitea release helper.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
2026-07-14 22:06:36 -05:00
co-authored by Cursor
commit 0d6b0b2f80
76 changed files with 5697 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# SyncGames planning archive
Frozen planning artifacts for posterity. Living operational docs live one level up in `docs/`.
| File | Contents |
|------|----------|
| [01-path-drafts.md](01-path-drafts.md) | Original three-path comparison (Syncthing / Gitea / MinIO) |
| [02-path3-implementation.md](02-path3-implementation.md) | Canonical Path 3 implementation plan (human-readable) |
| [syncgames_path_drafts_2b07b18f.plan.md](syncgames_path_drafts_2b07b18f.plan.md) | Cursor plan export (primary) |
| [syncgames_path_drafts_efddc87b.plan.md](syncgames_path_drafts_efddc87b.plan.md) | Earlier Cursor plan snapshot |
**Chosen path:** Path 3 (MinIO + session agents). Path 1 (hardened Syncthing) is the named fallback.
+63
View File
@@ -0,0 +1,63 @@
# Path drafts — three architectures
Original comparison before Path 3 was selected.
## Non-negotiable session model
Every viable design shares the same state machine. Continuous bidirectional sync of **live** save directories is the antipattern that caused prior LOPEs.
- Pull only at session start; push only at session end
- One lease per game
- Hash gate rejects stale WIP unless explicit force-restore
- Per-device history (35 versions); single live SSOT slot
- Never delete live without first moving current into history
### Conceptual layout
```
games/<game-id>/
live/ # SSOT current slot
history/<device>/ # last N snapshots per device
meta.json
```
---
## Path 1 — Hardened Syncthing + Session Orchestrator
Syncthing syncs only the **SSOT tree**, never native game save dirs. A local agent copies native ↔ WIP ↔ SSOT around sessions.
**Pros:** Fast LAN; mature Android Syncthing client.
**Cons:** Discipline required; peer conflict resolution if lease fails.
**Status:** Named **fallback** if MinIO/WAN path becomes blocking.
---
## Path 2 — Custom Agent + Gitea (Git LFS)
Gitea as SSOT; saves as LFS or release assets; agent pull/push around sessions.
**Pros:** Auditable history in web UI.
**Cons:** Git/LFS friction for binaries.
**Status:** Shelved.
---
## Path 3 — Custom Agent + MinIO (selected)
SSOT is versioned object storage on NAS MinIO. Agent implements pull-live / push-live / history / restore. Exposed via Cloudflare → NGINX → MinIO for WAN.
**Pros:** Cleanest binary SSOT model; no Git merge footguns; easy prune.
**Cons:** Own the agent end-to-end; need reachable HTTPS API.
**Status:** **Building this.**
### Comparison snapshot
| Tenet | Path 1 | Path 2 | Path 3 |
|-------|--------|--------|--------|
| Safety / direction | High if SSOT-only | High | Highest |
| Automation | Excellent Linux | Good | Excellent |
| Easy add/remove | CLI + Syncthing API | CLI + repo | CLI + key prefixes |
| LOPE recovery | history + .stversions | git/local history | object history |
| Android | Syncthing-Fork | Custom app | Custom Compose app |
| Past LOPE risk | Medium if live dirs synced | Low | Low |
+44
View File
@@ -0,0 +1,44 @@
# Path 3 implementation plan (canonical copy)
This is the human-readable archive of the Path 3 plan. The Cursor export snapshot lives beside this file.
## Locked decisions
| Decision | Choice |
|----------|--------|
| Primary | Path 3 — MinIO SSOT + session agents |
| Linux agent | Python CLI/daemon |
| Android | Kotlin + Jetpack Compose light UI |
| Remote access | WAN via Cloudflare (paid domain); no VPN |
| Ingress | Cloudflare → Tunnel → NGINX → MinIO |
| Fallback | Path 1 (Syncthing on SSOT tree only) |
| Planning | `docs/planning/` |
## Session ops
1. `start_session`: acquire lease → download `live/` → install to native paths → record WIP parent hash
2. `end_session`: ensure game idle → copy native → WIP → upload history → promote `live/` → update meta → release lease → prune
3. `restore`: copy chosen history prefix → `live/` (explicit)
4. `add` / `remove`: TOML + bucket prefixes / archive to `retired/`
## Object keys
```
games/<id>/live/<relative-save-path>
games/<id>/history/<device>/<iso-ts>/<relative-save-path>
games/<id>/meta.json
```
## CLI
```
syncgames add|remove|start|end|status|history|restore|doctor
```
## NGINX note
Treat MinIO as an S3 API vhost: preserve Host for SigV4, disable request buffering, large `client_max_body_size`, no `auth_basic` on S3 (use Cloudflare Access + MinIO keys). See `docs/nas-minio-cloudflare.md`.
## Out of scope (v1)
Path 2 Gitea, Windows-native agent, automatic binary merge, continuous sync of live game dirs.
@@ -0,0 +1,277 @@
---
name: SyncGames Path Drafts
overview: Build SyncGames on Path 3 (NAS MinIO + Python agent on Linux, light Android UI app). WAN access via Cloudflare Tunnel to paid domain. Path 1 is fallback. Planning markdown archived in-repo for posterity.
todos:
- id: scaffold-repo
content: Create SyncGames/ skeleton including docs/planning/ archive of all plan markdowns
status: in_progress
- id: archive-plans
content: Copy Cursor plan + path drafts into SyncGames/docs/planning/ for posterity
status: pending
- id: nas-minio-cloudflare
content: Document MinIO on NAS behind existing NGINX + Cloudflare Tunnel hostname, TLS, Access/auth, S3 path-style, nginx anti-grief settings
status: pending
- id: agent-core
content: Implement Python CLI/daemon - lease, pull-live, push-live, list-history, restore, add/remove game
status: pending
- id: safety-gates
content: Hash gate, atomic promote, version prune (N=3-5), refuse mid-write / mid-lease operations
status: pending
- id: linux-automation
content: systemd user units + process/Steam AppID watchers for auto start/end session (WAN endpoint same as laptop)
status: pending
- id: android-ui
content: Light Kotlin/Compose Android app - Start/End/History/Restore/Status talking MinIO over Cloudflare HTTPS
status: pending
- id: protocol-spec
content: Written protocol doc so Android and Python stay in lockstep on meta.json, leases, keys
status: pending
- id: path1-fallback-notes
content: Document Path 1 fallback trigger criteria and migration steps from MinIO SSOT tree
status: pending
- id: first-games
content: Seed configs for DS1/2/3, Elden Ring Seamless, Eden/Yuzu platform profiles
status: pending
isProject: false
---
# SyncGames — Path 3 Implementation Plan
## Locked decisions
| Decision | Choice |
|----------|--------|
| Primary architecture | **Path 3** — MinIO SSOT on NAS + session agents |
| Linux agent | **Python** CLI/daemon |
| Android client | **Light UI app** (Kotlin + Jetpack Compose) — not Termux as primary |
| Remote access | **WAN via Cloudflare** (paid domain) — not LAN-only, not VPN |
| Fallback | **Path 1** — Hardened Syncthing on SSOT tree only |
| Planning artifacts | **Kept in-repo** under `SyncGames/docs/planning/` |
Path 2 (Gitea) remains shelved unless explicitly reopened.
---
## Planning docs for posterity
On scaffold, archive all planning markdown into the repo so it survives outside Cursors plan UI:
```
SyncGames/docs/planning/
00-README.md # index of planning docs
01-path-drafts.md # original 3-path comparison
02-path3-implementation.md # this implementation plan (canonical copy)
syncgames_path_drafts_2b07b18f.plan.md # Cursor plan export snapshot
```
Update the archive whenever the plan materializes major revisions. Operational docs (`architecture.md`, `nas-minio.md`, etc.) stay separate under `docs/` as living guides.
---
## Session model (unchanged)
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Pulling: start_session
Pulling --> Ready: SSOT_copied_to_WIP
Ready --> Playing: game_running
Playing --> Pushing: end_session
Pushing --> Idle: SSOT_updated_and_versioned
```
- **Pull** only at session start; **push** only at session end
- One **lease** per game; second device blocked until release/expiry
- **Hash gate**: push rejected if WIP parent ≠ current live unless force-restore
- Per-device **history** keeps last N snapshots (default 5); live is a single slot
- Never sync the games native save directory continuously
---
## Path 3 architecture (WAN)
```mermaid
flowchart TB
subgraph clients [Clients_on_WAN]
PC[Linux_PC_Python]
Laptop[Laptop_Python]
Phone[Android_Compose_app]
end
subgraph cf [Cloudflare]
DNS[Paid_domain_DNS]
Tunnel[Cloudflare_Tunnel]
Access[Optional_CF_Access]
end
subgraph nas [NAS]
Nginx[NGINX_reverse_proxy]
MinIO[MinIO_S3_API]
end
PC --> DNS
Laptop --> DNS
Phone --> DNS
DNS --> Access
Access --> Tunnel
Tunnel --> Nginx
Nginx --> MinIO
```
All devices (desktop, laptop, phone) use the **same public HTTPS endpoint**, e.g. `https://syncgames-s3.example.com`. No LAN assumption, no VPN requirement.
**Ingress chain (matches your NAS pattern):** Cloudflare → Tunnel → **existing NGINX** → MinIO. NGINX stays the single reverse-proxy front door for all services; we do not bypass it.
### NGINX + MinIO — will it cause grief?
**Verdict:** No, if we treat MinIO as an S3 API vhost (not a generic web app) and apply the known proxy checklist. Most LOPE-adjacent failures from reverse proxies are **truncated uploads** or **broken SigV4 signatures** — both avoidable.
| Risk | Mitigation |
|------|------------|
| SigV4 `Host` mismatch | Public hostname must be what clients sign; NGINX must forward that Host (or MinIO must be configured for the same domain). Do not rewrite Host to `127.0.0.1`. |
| Truncated / buffered PUTs | `proxy_request_buffering off`; `client_max_body_size` large enough (e.g. 512m+); `proxy_http_version 1.1` |
| Expect: 100-continue quirks | `proxy_set_header Expect $http_expect;` or strip carefully per MinIO docs — test with a real multipart upload |
| Underscore / odd S3 headers | `ignore_invalid_headers off` on the MinIO server block |
| Chunked encoding / redirect loops | Prefer path-style addressing to one API hostname; avoid redirecting API → console |
| Console vs API confusion | Expose **S3 API only** on `syncgames-s3.<domain>`; keep MinIO Console on a separate optional vhost or LAN-only |
| Cloudflare body limits | Same as before — game saves are usually fine; document if huge dumps fail |
| Double TLS / WebSockets | Console needs Upgrade headers; **API path used by SyncGames does not need WebSockets** |
**Recommended NGINX shape (to ship in `docs/nas-minio-cloudflare.md`):** dedicated `server_name syncgames-s3.<domain>`; `location /``http://127.0.0.1:9000` (MinIO API); forwarding `Authorization`, `X-Amz-*`, `Content-Type`, `Content-Length`, `X-Forwarded-Proto https`; no `proxy_buffering` on uploads; no auth_basic in front of S3 (breaks SDK) — use Cloudflare Access and/or MinIO keys instead.
Agents point at `https://syncgames-s3.<domain>` with path-style S3. `syncgames doctor` will include a PUT/GET round-trip probe to catch NGINX misconfig early.
### Cloudflare exposure (chosen approach)
1. Run **MinIO** on NAS private bind (e.g. localhost:9000)
2. Add **NGINX** vhost for the SyncGames S3 hostname (same pattern as your other services)
3. Point **cloudflared** at NGINX (or at the existing tunnel ingress that already hits NGINX), not directly at MinIO, so all services stay consistent
4. Map `syncgames-s3.<domain>` on the paid domain
5. Prefer **Cloudflare Access** in front (defense-in-depth); MinIO access key / secret still required
6. Document path-style addressing for boto3 + Android AWS SDK
7. Note CF upload size limits; fallback guidance if an emulator dump exceeds them
Laptop and phone never need to be on home LAN.
---
### Object key layout
```
games/<game-id>/live/<relative-save-path>
games/<game-id>/history/<device-id>/<iso-ts>/<relative-save-path>
games/<game-id>/meta.json
```
`meta.json` fields (minimum): schema version, live checksums, parent hash, lease holder device id, lease expires_at, versions_to_keep, updated_at.
### Ops
1. **start_session**: acquire lease → download `live/` → install to native save path(s) → record WIP parent hash locally
2. **end_session**: wait until game not writing → copy native → WIP → upload history prefix → promote to `live/` → update `meta.json` → release lease → prune old history beyond N
3. **restore**: user picks history object prefix → copies to `live/` (explicit only)
4. **add / remove game**: declarative TOML + bucket prefix init / archive-to-`retired/`
### Shared protocol spec
`docs/protocol.md` is the source of truth for key layout, lease rules, and hash gates so **Python and Android stay compatible** without sharing a runtime.
### Agent CLI surface (Linux)
```bash
syncgames add --name "..." --platform steam --paths "..." --versions 5
syncgames remove <game-id>
syncgames start <game-id>
syncgames end <game-id>
syncgames status [<game-id>]
syncgames history <game-id>
syncgames restore <game-id> --from <device>/<ts>
syncgames doctor
```
---
## Repo skeleton
```
SyncGames/
README.md
docs/
planning/ # posterity: path drafts + plan snapshots
architecture.md
protocol.md
add-game.md
nas-minio-cloudflare.md
fallback-path1.md
config/
devices.toml
agent.toml # Cloudflare HTTPS endpoint, bucket, device-id
games/*.toml
templates/game.toml
agent/ # Python package (Linux PC + laptop)
android/ # Kotlin/Compose light UI app
systemd/
syncgames-agent.service
[email protected]
```
---
## Android light UI — yes, in scope for v1
**Answer:** Yes. Implement Android as a **light dedicated UI app**, not Termux-first.
**Stack:** Kotlin + Jetpack Compose + AWS S3 / MinIO-compatible SDK over HTTPS to the Cloudflare hostname.
**Screens (minimal):**
- Game list + lease/status badge
- **Start session** (pull + lease + write into configured save dirs)
- **End session** (push + history + release)
- **History** + **Restore** (explicit confirm)
- Settings: endpoint URL, keys (prefer Android Keystore), device id, game path pickers via Storage Access Framework
**Deployability:** sideload APK or simple GitHub/Gitea release; configure once with Cloudflare URL + credentials; play flow is button → play → button.
**Why not Termux as primary:** harder to hand to “just use it” and weaker UX for safeties (confirm restore, show lease holder). Termux may remain a documented escape hatch only.
Linux still owns `add`/`remove` game and systemd automation; Android focuses on session buttons + restore for emulator titles on phone.
---
## Linux automation
- systemd user service runs the Python agent
- Per-game watcher: Steam AppID and/or process name from game TOML
- `start` / `end` on process lifecycle
- Endpoint in `agent.toml` is always the Cloudflare WAN URL (same as laptop/phone) for consistent behavior away from home
---
## Path 1 fallback triggers
- Cloudflare/MinIO WAN path too unreliable or blocked by CF limits
- Android app file-access friction exceeds Syncthing-Fork convenience **and** you accept SSOT-dir-only Syncthing rules
**Migration:** export MinIO `live/` + `history/` to a folder tree; Syncthing that tree only; keep Python session agent for native ↔ SSOT copies.
---
## Seed game configs (after agents work)
- Dark Souls 1 / 2 / 3 — Steam / Proton + Seamless Coop paths
- Elden Ring — Seamless Coop paths
- Eden / Yuzu — Android + Linux emulator save profiles
---
## Out of scope for v1
- Path 2 Gitea
- Windows-native agent
- Automatic binary merge
- Continuous sync of live game directories
- VPN / LAN-only client modes as a requirement (WAN Cloudflare is the path)
---
## Paths 12 for posterity
Full Path 1 and Path 2 writeups are archived under `docs/planning/01-path-drafts.md` when the repo is scaffolded — not only in chat history.
@@ -0,0 +1,240 @@
---
name: SyncGames Path Drafts
overview: Greenfield SyncGames in a new `SyncGames/` directory. Below are three deployable architectures that all enforce the same session model (pull SSOT → play WIP → push SSOT) while differing in transport/storage. Pick one path before implementation.
todos: []
isProject: false
---
# SyncGames — Three Architecture Paths
## Non-negotiable session model (all paths)
Every viable design shares the same state machine. Continuous bidirectional sync of **live** save directories is what caused your prior LOPE; none of these paths do that.
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Pulling: start_session
Pulling --> Ready: SSOT_copied_to_WIP
Ready --> Playing: game_running
Playing --> Pushing: end_session
Pushing --> Idle: SSOT_updated_and_versioned
note right of Pulling
Refuse start if another device
holds lease or SSOT is dirty
end note
note right of Pushing
Snapshot WIP into device history
then promote to live SSOT slot
end note
```
**Shared layout (conceptual):**
```
SyncGames/
games/<game-id>/
live/ # SSOT current slot (one winner)
history/
<device-id>/ # last N snapshots per device (default 5)
meta.json # hash, mtime, device, session lease, schema
config/
games/*.toml # declarative game defs
devices.toml
```
**Shared guardrails:**
| Rule | Behavior |
|------|----------|
| Direction | Pull only at session start; push only at session end |
| Lease | One active session per game; second device blocked until lease expires/release |
| Hash gate | Reject push if local WIP hash != expected parent (stale WIP) unless explicit `--force-restore` |
| Version retention | Keep N snapshots per device (configurable 35); prune oldest after successful new snapshot |
| Never delete live | Promote via atomic rename/copy; old live moves into history first |
| Game offline | Automation watches process exit / Steam AppID, never mid-write |
**Add-game workflow (identical UX across paths):**
```bash
syncgames add \
--name "Elden Ring (Seamless)" \
--platform steam \
--paths "~/.steam/.../SeamlessCoop/*.co2" \
--versions 5
```
**Remove-game:** archive `games/<id>/` to `retired/<id>-<date>/`, stop watchers, revoke folder/remote access.
---
## Path 1 — Hardened Syncthing + Session Orchestrator
**Idea:** Syncthing only syncs the **SSOT tree** (`live/` + `history/` + `meta`), never the games native save directory. A local agent copies between native save path ↔ WIP ↔ SSOT around sessions.
```mermaid
flowchart LR
NativeSave[Game_native_save_path]
WIP[Local_WIP_staging]
SSOT[SSOT_folder_Syncthing]
OtherDev[Other_devices]
NativeSave -->|"end_session copy"| WIP
WIP -->|"snapshot + promote"| SSOT
SSOT <-->|"Syncthing send-receive on SSOT only"| OtherDev
SSOT -->|"start_session copy"| WIP
WIP -->|"start_session install"| NativeSave
```
**Syncthing config hardening:**
- Folder type: normal send-receive **only** on `SyncGames/games/` (or per-game subfolders)
- Enable **Simple File Versioning** (Keep Versions ≥ 5) as a second safety net behind custom history
- `.stignore` excludes `*.tmp`, lock files, WIP staging outside the synced tree
- Do **not** sync Proton/prefix save dirs directly
- Optional always-on home server / NAS as Receive-favoring mirror with staggered versioning
**Automation:** Linux systemd user units + process watchers (Steam AppID / executable). Android: Syncthing-Fork for SSOT folder + Termux/foreground service or a thin companion app for “Start session / End session” when full auto is harder.
| Pros | Cons |
|------|------|
| Fast LAN sync you already know | Must never point Syncthing at live saves (discipline + tooling) |
| Mature Android client for files | Peer conflict resolution still exists if lease protocol fails |
| Low custom infra | Orchestrator is still custom software |
**Best if:** You want maximum automation across Linux + Android with minimal self-hosted backend.
---
## Path 2 — Custom Agent + Gitea (Git LFS / raw releases)
**Idea:** Your Gitea instance is SSOT. Each game is a small repo (or one monorepo with per-game dirs). Saves live as LFS objects or release assets. Agent does pull/checkout → WIP, and commit+push / release upload on end.
```mermaid
flowchart LR
Agent[syncgames_agent]
WIP[Local_WIP]
Gitea[Gitea_repo_LFS]
Hist[Device_history_local]
Agent -->|"git pull / download live tag"| WIP
WIP -->|"play"| Game[Game]
Game --> WIP
WIP -->|"snapshot"| Hist
Agent -->|"commit live + push"| Gitea
```
**Hardening vs naïve git:**
- Treat `live/` as a single tracked blob set; never merge binary conflicts — lease + parent-hash check abort the push
- Store history as `history/<device>/<timestamp>/` commits **or** local-only tarballs with only `live/` pushed (cleaner; history survives offline)
- Prefer **Git LFS** for binary saves; optional signed tags `live/<game-id>` as the authoritative pointer
- Manual “Sync Now” buttons remain available; automation calls the same API
**Android:** Kotlin/Compose or Flutter thin client using Gitea HTTP API + LFS (avoid full git on phone if painful).
| Pros | Cons |
|------|------|
| Explicit, auditable history in Gitea UI | Git/LFS friction for large/frequently-changing binaries |
| Works over WAN without Syncthing mesh | Needs always-reachable Gitea |
| Matches “button press = state change” mental model | Automated push still needs the same lease logic you fear forgetting |
**Best if:** You already run Gitea and want human-readable restore via web UI more than raw speed.
---
## Path 3 — Custom Agent + Object Store SSOT (MinIO / S3 / rclone remote)
**Idea:** Skip git entirely. SSOT is versioned object storage you control (self-hosted MinIO on your LAN/homelab, or any S3-compatible bucket). Agent implements `pull-live`, `push-live`, `list-history`, `restore` against object keys.
```mermaid
flowchart TB
subgraph devices [Devices]
PC[Linux_PC_agent]
Laptop[Laptop_agent]
Phone[Android_agent]
end
subgraph ssot [SSOT_MinIO]
LiveKey["games/er/live/*"]
HistKeys["games/er/history/device/ts/*"]
Meta["games/er/meta.json + lease"]
end
PC --> ssot
Laptop --> ssot
Phone --> ssot
```
**Object layout:**
```
games/<id>/live/<relative-save-path>
games/<id>/history/<device>/<iso-ts>/<relative-save-path>
games/<id>/meta.json # checksums, parent, lease holder, expires_at
```
**Ops model:**
- Push: upload WIP → new history prefix → checksum → atomic update of `live/` + `meta.json` (lease required)
- Pull: verify lease available → download `live/` → install into native path → mark WIP parent hash
- Restore: copy chosen history prefix → `live/` (explicit user action)
- Transport: MinIO SDK, or `rclone` wrapped so you can swap backends later
**Automation same as Path 1** (session hooks); storage is just S3 semantics instead of Syncthing/Git.
| Pros | Cons |
|------|------|
| Cleanest model for binary SSOT + versioned keys | Needs MinIO (or cloud S3) reachable from phone |
| No git binary-conflict footguns | You own the agent end-to-end |
| Easy prune (delete old prefixes) | Slightly more DIY than Syncthing for LAN presence |
**Best if:** You want the strongest conceptual match to “one live slot + per-device history” without Syncthings merge semantics or Gits binary awkwardness.
---
## Comparison against your tenets
| Tenet | Path 1 Syncthing+Agent | Path 2 Gitea+Agent | Path 3 MinIO+Agent |
|------|------------------------|--------------------|--------------------|
| Safety / controlled direction | High if SSOT-only synced | High (hash+lease) | Highest (explicit keys+lease) |
| Automation | Excellent on Linux; good on Android | Good; WAN-friendly | Excellent; WAN-friendly |
| Easy add/remove game | CLI generates folder + Syncthing API | CLI creates repo/paths | CLI creates key prefixes |
| LOPE recovery | device history + `.stversions` | git history / local history | object history prefixes |
| Android friction | Lowest (Syncthing exists) | Medium (custom app) | Medium (custom app / rclone) |
| Your past LOPE risk | Medium — only if someone resyncs live dirs | Low | Low |
---
## Recommended default (if you want a pick)
**Path 3 (MinIO + session agent)** as the core SSOT design — it maps 1:1 to live slot + per-device history, avoids Syncthing merge and Git LFS pain, and still supports full automation via process hooks.
**Optional hybrid later:** Path 1s Syncthing can mirror a MinIO bucket backup, or Path 3s `history/` can be additionally restic-backed for offsite retention. That is additive hardening, not required for v1.
---
## Proposed SyncGames repo skeleton (whichever path)
```
SyncGames/
README.md
docs/architecture.md
docs/add-game.md
config/games/ # declarative TOML per title
config/devices.toml
agent/ # Python or Go CLI + daemon
templates/game.toml
systemd/ # user units for watchers
```
Initial titles in config templates: Dark Souls 1/2/3, Elden Ring (Seamless Coop paths), Eden/Yuzu NAND/save dirs as separate platform profiles.
---
## Decision needed before implementation
Reply with which path to build first (**1**, **2**, or **3**). Also confirm:
1. Is a always-on homelab box available for MinIO/Gitea/Syncthing hub?
2. Primary phone OS for sync — Android only?
3. Prefer agent language: **Python** (fast to ship) or **Go** (single static binary)?