Session-gated MinIO save sync with AppImage GUI, CLI edit/session flow, and Gitea release helper. Co-authored-by: Cursor <[email protected]>
11 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| SyncGames Path Drafts | 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. |
|
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 Cursor’s 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)
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 game’s native save directory continuously
Path 3 architecture (WAN)
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)
- Run MinIO on NAS private bind (e.g. localhost:9000)
- Add NGINX vhost for the SyncGames S3 hostname (same pattern as your other services)
- Point cloudflared at NGINX (or at the existing tunnel ingress that already hits NGINX), not directly at MinIO, so all services stay consistent
- Map
syncgames-s3.<domain>on the paid domain - Prefer Cloudflare Access in front (defense-in-depth); MinIO access key / secret still required
- Document path-style addressing for boto3 + Android AWS SDK
- 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
- start_session: acquire lease → download
live/→ install to native save path(s) → record WIP parent hash locally - end_session: wait until game not writing → copy native → WIP → upload history prefix → promote to
live/→ updatemeta.json→ release lease → prune old history beyond N - restore: user picks history object prefix → copies to
live/(explicit only) - 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)
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/endon process lifecycle- Endpoint in
agent.tomlis 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 1–2 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.