main
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9074121898 |
feat: 'disable' renames to sync.json.disabled; new 'enable' rename-back
Pause/resume sync without losing the instance_id.
disable sync.json -> sync.json.disabled
enable sync.json.disabled -> sync.json
Re-enabling preserves the original ULID + url so the same restic
repo continues. No new instance_id minted, no orphaned snapshot
history. Tradeoff vs the previous 'disable = delete' semantics:
the on-disk artifact survives, so a truly fresh start now needs
'disable && rm sync.json.disabled' before 'setup'.
Implementation:
config.disable(pack) os.rename(sync.json -> sync.json.disabled).
False if no sync.json.
config.enable(pack) os.rename(sync.json.disabled -> sync.json).
Refuses if sync.json already exists
(caller must disable first).
config.delete(pack) now sweeps BOTH forms (escape hatch / tests).
setup_flow gains a precheck: if sync.json.disabled is present, point
the user at 'enable' instead of silently minting a fresh ULID over
their existing instance.
Opt-in gate (cfgmod.exists) is unchanged — only literal sync.json
counts. The .disabled sibling is invisible to pull/push, so the
silent-no-op behavior for paused instances Just Works.
cli adds 'enable' subcommand alongside 'disable'. _run_disable prints
'already disabled' when called twice; _run_enable refuses to clobber
an active config (exits 2 with the FileExistsError message).
7 new tests for disable/enable behavior + edge cases (idempotency,
nothing-to-X, refuse-clobber). 80 tests total.
|
||
|
|
20cfdf62f2 |
feat: opt-in by sync.json + per-instance ULID + restic subpath
Reshapes the launcher integration around two ideas:
1. ONE global Prism PreLaunch/PostExit hook is enough for all
instances. Wire it once at Settings > Default > Custom commands:
python /opt/cloud-sync.pyz pull --pack-folder=$INST_MC_DIR
python /opt/cloud-sync.pyz push --pack-folder=$INST_MC_DIR
Instances WITHOUT .cloud-sync/sync.json are silent no-ops (rc=0,
no UI, no banner). The opt-in probe runs BEFORE the UI factory
so Prism's launch log stays clean for non-sync instances.
2. Per-instance opt-in via 'setup' / 'init' subcommands that mint a
fresh ULID-style instance_id + write sync.json (mode 644) and
token (mode 600). 'disable' removes sync.json; cloud data
untouched.
Restic URL gains an /<instance_id>/ subpath under the user's
namespace, so two Prism instances of the same Discord user no longer
share a snapshot timeline. --private-repos still gates on the first
path segment (the username); deeper segments are user-controlled,
so this works without server-side coordination. First-push-on-a-new-
instance probes via 'restic cat config' and 'init's the per-instance
repo if absent.
UI label resolution is runtime-only (NEVER stored in sync.json) so
the user renaming the Prism instance just propagates through on
next launch:
--instance-label > $INST_NAME > $INST_ID > instance_id[:8]
Schema bumps:
state.json schema: 1 -> 2, adds instance_id field. Schema-1 files
are treated as missing (existing test1 user re-pulls fresh).
sync.json schema: 1 (new file).
CLI rework:
pull / push no --url; load everything from sync.json
setup interactive: Qt login dialog for token; URL prompt
if --url omitted; falls back to stdin when headless
init non-interactive setup; for scripted callers
disable rm sync.json
Args dataclass: drops 'url', adds 'instance_label'. cli.parse() now
returns (cmd, Namespace); a separate args_from(ns) builds the Args
so each subcommand can pluck the bits it needs from the Namespace
without forcing a 'one Args fits all subcommands' shape.
73 tests green; pyz 75 KB.
Smoke-verified locally:
- pull/push on a folder without sync.json: silent rc=0, no banner
- init writes sync.json (644) + token (600) with correct contents
- disable removes sync.json, keeps token
- mint produces unique 26-char base32 instance_ids
- label resolution chain (flag > INST_NAME > INST_ID > prefix)
|
||
|
|
b31fdd023a |
rename: cloud sync -> instance sync; cloud -> Timemachine Network; drop Tk
Product / UI / CLI / docs rebrand. Internal package, repo, and on-disk dir names stay 'cloud_sync' / 'cloud-sync' / '.cloud-sync/' to avoid breaking existing installs; a future commit can do the file-system rename when the cost is worth paying. User-facing changes: CLI prog name: cloud-sync -> instance-sync CLI description: cloud-svc URL -> Timemachine Network endpoint Dialog title: CLOUD SYNC -> INSTANCE SYNC Dialog title: CLOUD CONFLICT -> INSTANCE CONFLICT Dialog title: CONNECT CLOUD SAVE -> CONNECT TO THE NETWORK Card label: Cloud Save -> Remote Save Skip button: Skip cloud sync -> Skip instance sync Body copy: 'the cloud' -> 'the Timemachine Network' Window titles: Cloud sync — ... -> Instance sync — ... Log prefix: cloud-sync: -> instance-sync: Error prose: 'cloud-sync token' -> 'instance-sync token' Backend changes: restic --host tag: cloud-sync -> instance-sync State.host_tag dflt: cloud-sync -> instance-sync (Existing snapshots with the old tag still pull fine; we use 'latest'.) Drop tkinter fallback: ui.py now offers Qt OR Headless. tkinter is unnecessary given we already maintain Qt + headless; one less code path to keep styled, smaller pyz. make_progress() picks Qt first, falls through to HeadlessProgress on ImportError with a stderr hint to 'pip install PySide6'. README: rebrand title + prose; note repo/dir rename deferred; call out the PySide6 install step. Conflict/login dialogs are now Qt-only; without Qt, conflict aborts (defensive) and login tells the user to paste the token manually. 52 tests green; no test-file label changes needed since they only exercise internal APIs. |
||
|
|
f1cb9f4b86 |
feat(ui): inline SVG icons across all three dialogs
New module cloud_sync/icons.py:
- CLOUD_SVG, STORAGE_SVG: card glyphs (Material Icons, Apache-2.0)
- WARNING_BADGE_SVG, PLUS_BADGE_SVG, SYNC_BADGE_SVG: header badges
(hand-rolled — circle + vector primitives, no glyph font dependency)
- svg_pixmap(svg, size): QSvgRenderer-backed rasteriser; dual-binding
helper falls back from PySide6 to PyQt6.QtSvg.
Wired into ui_qt.py:
- Conflict dialog: warning badge SVG (light disc + dark !), cloud
card SVG, storage card SVG. Drops the QSS circle hack — entire
badge is one pixmap now.
- Login dialog: plus badge SVG (Prism-green disc + dark +).
- Progress window: sync arrow badge SVG (Prism-green disc + circular
arrow + head).
QSS shed: no more bg/border-radius/font-size acrobatics for badges
since the SVG includes the disc. Card icons are crisp at any DPI.
52 tests still green; pyz grows ~5 KB for the SVG strings + the new
icons module.
|
||
|
|
7c9d33f952 |
feat(sync): wire state.json + divergence detection + dialogs
state.py: per-instance sync state. <pack>/.cloud-sync/state.json
(mode 600) records last_pulled_snapshot_id + last_pulled_at +
host_tag. Versioned schema. clear() on remote-empty.
sync.pull decision tree (replaces the unconditional restore):
no token file
→ prompt_login_qt; on Skip return 0 (don't block launch)
no state + remote empty
→ no-op
no state + remote non-empty
→ restore (first-run on this machine)
state.id == remote.id
→ skip restore (up to date)
state.id != remote.id, no in-scope local edits since state.at
→ restore (fast-forward)
state.id != remote.id, in-scope local edits since state.at
→ prompt_conflict_qt
keep_local → don't restore; push will overwrite cloud
use_remote → restore + update state
cancel → exit 1
sync.push: --json output parsed for snapshot_id; state.json updated
to that id after a successful backup. Skips silently if no token.
_find_modified_in_scope: walks include roots, filters via
_matches_any (restic-style globs: dir/, **/dir/, **/*.glob).
Stops at 50 hits; we only need 'any' + a sample for the dialog.
_format_dt: hand-rolled (no GNU-vs-Windows strftime quirks) →
'Thursday, October 21, 2021 at 7:12 PM'.
Restic JSON parsing helpers: _parse_snapshots, _parse_restic_time
(handles nanosecond precision), _parse_backup_summary.
tests/test_state.py: 19 new tests covering state read/write, scope-
aware mtime walk, exclude glob matching, restic output parsers.
Total: 52 green.
|
||
|
|
fa04fade74 |
style(ui): progress window matches conflict + login
Same skeleton: Prism-green badge ('↻') + uppercase title in the
header row, status text under it, indeterminate progress bar (dark
inset with Prism-green chunk), Cancel button bottom-right styled
as the shared 'secondary' button.
Bumped from 440x160 to 520x240 so the header reads at the same
weight as the other two dialogs. title.upper() applied in run_with
so callers can keep passing 'Cloud sync — pulling' without manual
uppercasing.
Three dialogs now share one visual language: Prism dark surface
(#313131), Prism-green accent (#96db59), monospace for code/data,
circled badge in the top-left of every header.
|
||
|
|
aba6472292 |
style(ui): login dialog matches Steam-layout in Prism dark
Same skeleton as the conflict dialog: header (Prism-green badge +
uppercase title), body copy, content, foot row.
Specifics:
- badge: '+' on Prism-green circle (vs the conflict's '!' on
light gray) — green reads as 'setup / new', not warning
- command callout: monospace, green text, dark inset card
- token input: monospace, dark inset, green focus border, green
selection highlight
- 'Skip cloud sync' = secondary (dark surface, green hover border)
- 'Save and continue' = primary (Prism-green fill, black text)
Same Enter-to-submit + format validation behavior as before;
on-screen error reuses Prism's BrightText red.
|
||
|
|
016c88f60f |
style(ui): conflict dialog in Prism dark palette
Same Steam-Cloud-Conflict layout (header glyph + uppercase title + two card-buttons + Cancel) but recolored to Prism's palette: background #313131 (Prism Window) cards #2a2a2a (Prism AlternateBase) card hover #383838 + #96db59 border (Prism Highlight green) cancel button #303030 + #96db59 border on hover Warning glyph keeps the light circle (#d6dde6) for high contrast against the dark surface — gives the same 'pay attention' weight without forcing a different theme just for one dialog. |
||
|
|
98c63a261b |
feat(ui): Steam-style conflict dialog
Replaces the radio+continue layout with a Steam Cloud Conflict
lookalike: uppercase title with circled-! glyph, three-line body
copy, two large card-buttons (Cloud Save / Local Save) each showing
icon + label + 'Modified <date>', Cancel bottom-right. Card click
commits the choice and dismisses — no separate Continue button.
Background switched to Steam-blue (#1b2838) instead of Prism dark
so the dialog reads as a 'pay attention' moment rather than another
status pane.
Card glyphs are unicode placeholders (☁ / ▢) — easy SVG swap later
if we want crisp HDD/cloud icons.
API change:
prompt_conflict_qt(local_summary, remote_summary)
→ prompt_conflict_qt(local_modified, remote_modified, save_label)
Caller now passes pre-formatted timestamp strings + an optional
noun phrase ('Minecraft save').
|
||
|
|
d4c76a4e37 |
feat(ui): login + conflict dialogs (Qt)
Login dialog (prompt_login_qt): Modal asking for a discord_id:password token. Echo masked. Validates format before accepting. 'Skip cloud sync' returns None so caller can bypass sync without blocking the launch. Conflict dialog (prompt_conflict_qt): Modal with three radio options — keep_local, use_remote, cancel. Shown when local files were modified since the last pulled snapshot AND the remote has a newer snapshot from another host (divergence state.json detection happens in sync.pull; not wired yet). Both reuse the Prism dark palette via _apply_prism_dark. Tk fallbacks not implemented — Qt is the path most players will hit. Next: state.json (last_pulled_snapshot_id), divergence detection in sync.pull, integration with these dialogs. |
||
|
|
fe26ed309c |
feat(ui): Qt progress window with Prism-Launcher-inspired dark palette
cloud-sync now ships a real Qt UI alongside the tkinter fallback.
Architecture:
- HeadlessProgress: --no-gui path, plain stdout
- TkProgressWindow: stdlib fallback when Qt isn't installed
- QtProgressWindow: preferred path; supports both PySide6 and PyQt6
(interchangeable APIs for our subset)
The factory in ui.py picks Qt → tkinter → headless. Tk stays so the
zipapp still works on bare Python with no extras.
Threading: QApplication runs on the main thread (started by run_with
via QDialog.exec). The restic worker runs on a daemon threading.Thread.
Cross-thread UI updates go via a Signal on a bridge QObject so Qt
auto-marshals them onto the main thread via a queued connection.
Cancellation: WM close + Cancel button both set a flag. sync.pull/push
pass ui.is_cancelled as restic.run's cancel_check; the subprocess gets
killed and returns -1 → exit 1.
Theme: Fusion style + Prism's dark palette (RGB values copied as facts
from PrismLauncher's DarkTheme.cpp). Override with PRISM_THEME=off.
Pyz size went 20 KB → 36 KB (added ui.py + ui_qt.py).
33 tests still green.
|
||
|
|
49d1cb3280 |
drop restic repo encryption; rely on TLS + append-only + LUKS
User credentials now serve HTTP basic auth only. Repos init with --insecure-no-password. Removes: - RESTIC_PASSWORD env in client subprocess - Per-repo password coordination story - Multi-key restic setup (user key + operator-master key) - Two-password recovery edge cases Operator-side prune now runs over the filesystem path (-r /srv/.../<user>/) which bypasses rest-server's HTTP-layer append-only enforcement. No password needed at all. Protection model stays: - TLS in transit (reverse proxy) - HTTP basic per-user (htpasswd) for read/write authorization - --private-repos for per-user URL isolation - --append-only for client-side delete protection - LUKS / disk-level for at-rest encryption (operator's responsibility) Verified end-to-end on john: pull → push → restore round-trip works, DELETE on bogus snapshot still returns 403 (append-only intact), operator can read repo via filesystem path (prune-mode access works). 33 pytest still green. |
||
|
|
ffdfb1f9b6 |
pivot to Python: replace Kotlin/JVM with stdlib zipapp
Reasons stacked up:
- AV: unsigned JARs that auto-download binaries + upload files trigger
Windows Defender false-positives more often than Python scripts
invoked by code-signed python.exe.
- Qt UI option: PySide6 opens a path to a real Qt UI (matching Prism's
look) if needed later. JVM Qt bindings are abandoned.
- frazclient already needs Python; inlining as 'import cloud_sync' is
zero overhead vs the launcher always shelling out to java.
Implementation:
- cloud_sync package: cli.py (argparse), creds.py, scope.py,
restic.py (binary discovery + auto-download + sha256 verify),
sync.py (pull/push subprocess restic).
- pyproject.toml with hatchling backend; pip-installable.
- Makefile builds cloud-sync.pyz via python -m zipapp (~53 KB).
- 33 pytest tests, stdlib only on runtime.
- CI workflow runs pytest matrix (3.10/3.11/3.12) + builds pyz.
- DESIGN.md + README.md updated to reflect Python.
E2E verified against local restic-rest-server:
pull empty → push initial → rm -rf local → pull restores → modify+push
creates second snapshot → client forget --prune blocked by --append-only.
Throws away ~565 LOC of Kotlin (and 18 jar tests) committed earlier in
this same session. Net result is ~250 LOC Python + 33 tests = smaller
and more aligned with the rest of the stack.
|