559 Commits

Author SHA1 Message Date
Markus e68dcd7b92 Merge pull request #127 from TZERO78/docs/readme-logo-header
docs(readme): replace H1 with centred logo banner
2026-05-25 12:38:55 +02:00
TZERO78 ddd44c3775 docs(media): add logo assets — 800x200 banner, 1280x640 social preview,
1024x1024 master, favicon

- kopi-docka-800x200.png (72 KB): wide README header banner — used by
  the redesigned title in the preceding commit
- kopi-docka-1280x640.png (177 KB): GitHub Open Graph aspect — upload
  via Repo Settings → Social preview to control the link card on
  X/HN/Reddit/Discord (manual UI step, not scriptable via gh CLI)
- kopi-docka-logo.png (1.2 MB): 1024x1024 master for downstream
  derivations (presentations, DR-bundle templates)
- kopi-docka-favicon.png (168 KB): 420x420 for any future doc site

Total +1.58 MB to the repo. All four sit alongside the existing
demo-*.svg files under docs/media/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:35:57 +00:00
TZERO78 e09886a2bb docs(readme): replace title text with centred logo banner
Swap the plain "# Kopi-Docka" H1 plus tagline blockquote for the 800x200
project banner from docs/media/, centred via HTML <p>, with badges and
tagline arranged horizontally below it. Visual identity instead of
straight markdown headers — same pattern modern Python projects
(FastAPI, Pydantic, Ruff) use.

The banner image already carries the project name visually; alt text
provides accessibility / screen-reader fallback for when the image
fails to load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:07:40 +00:00
Markus 2f0e754a11 Merge pull request #126 from TZERO78/docs/windows-stream-warning
docs: flag Windows PowerShell binary-stream corruption + AES-ZIP extraction
v7.6.4
2026-05-25 11:44:36 +02:00
TZERO78 9746b6993e release: v7.6.4 — Windows DR-stream guidance + combined demo SVG
Docs / help-text only. Flags PowerShell's `>` binary corruption pitfall
in the --stream help text and in DISASTER_RECOVERY.md, plus a 7-Zip
extraction note for AES-256 archives. Combines the 7 demo SVGs from
v7.6.2 into one 113-second sequential animation to keep the README
scrollable.

No code path, config, or repository format changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:43:09 +00:00
TZERO78 3193baeec4 docs(readme): collapse 7 demo SVGs into one sequential animation
7 individual SVG embeds back-to-back made the README quite scroll-heavy.
This combines all 7 casts into a single 113-second animation that plays
the scenes sequentially with brief banner cards between them
("1 / 7 · doctor · system health check", etc.), then loops.

The individual SVG files are preserved (other docs may reference them
directly) and exposed as a collapsible "Individual scenes" table below
the combined animation, so the user-flow narrative is still visible at
a glance without expanding anything.

Combined SVG is 743 KB — larger than any single scene but acceptable
inline; GitHub renders animated SVGs without lazy-loading anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:33:18 +00:00
TZERO78 95c1a9068c docs: flag PowerShell binary-stream corruption + Windows AES-ZIP extraction
Real user hit this on Windows: PowerShell 5.1's `>` operator re-encodes
stdout as UTF-16, doubling the byte count and corrupting the AES ZIP
beyond recognition. Even 7-Zip rejects the file ("not an archive").
Confirmed: locally a clean stream is 7 KB; the PS-corrupted version
arrives at 14 KB. cmd.exe's `>` works correctly.

Three touch-points so this is hard to miss:

1. `--stream` help text in disaster_recovery_commands.py now mentions
   the PowerShell pitfall and points at docs/DISASTER_RECOVERY.md.
2. README.md gains an inline Windows note next to the stream example
   plus a stronger 7-Zip hint at extract time.
3. docs/DISASTER_RECOVERY.md "SSH Stream Mode" section grows two
   subsections: "Windows clients" (scp + cmd.exe patterns, verification
   command) and "Extracting on Windows" (Explorer's lack of AES-256
   support, recommended tools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:27:01 +00:00
Markus 3ce7813491 Merge pull request #124 from TZERO78/docs/readme-demo-svgs
docs(readme): add animated SVG demos for dry-run + doctor (HN-prep)
2026-05-25 11:02:34 +02:00
Markus 283759705b Merge pull request #125 from TZERO78/fix/v7.6.3-dr-stream-console
release: v7.6.3 — fix DR --stream Console.print(err=True) crash
v7.6.3
2026-05-25 11:01:31 +02:00
TZERO78 0e30400884 test(dr): mock kopia dependency in stream tests for CI
CI runners don't have kopia binaries; the command's dependency-check
exited before reaching the streaming branch, so both stream regression
tests failed there even though the code path under test was correct.

Mock DependencyHelper.exists() (and the DR manager) so the test
focuses on the streaming-Console-print branch the v7.6.3 fix targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:59:50 +00:00
TZERO78 6aa1ec2d49 release: v7.6.3 — fix DR --stream Console.print(err=True) crash
Rich's Console.print() does not accept an `err=` kwarg; every
`disaster-recovery export --stream` invocation blew up with TypeError
before any ZIP byte hit stdout. Fix: bind a separate
`Console(stderr=True)` for the 3 streaming-path messages.

Reproduced over SSH against the testlab. Regression covered by 2 new
tests in test_disaster_recovery_commands.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:56:29 +00:00
TZERO78 9d0c84a2bb docs(readme): swap DR demo to richer legacy-command transcript
The bare 'disaster-recovery export ... --yes' demo was too thin — one
panel, just file path and decrypt commands. The legacy
'disaster-recovery' (no subcommand) path triggers a more substantive
flow that's actually nicer for HN/r/selfhosted visitors:

  1. Deprecation-warning panel → demonstrates the tool actively
     guides users toward the new ZIP format (a trust signal — it
     doesn't just silently break)
  2. "Disaster Recovery Bundle Creation" intro panel
  3. Progress log (rclone.conf added, password sidecar warning)
  4. "Bundle Created" panel with all three file paths and the
     openssl-decrypt command inline

Source: real transcript provided by the user against the testlab,
faithfully reconstructed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:46:02 +00:00
TZERO78 f21875085b docs(readme): add history, snapshot-list, and DR-export demo SVGs
Completes the narrative arc on the README from "setup the tool" to
"recover from total loss":

  Doctor → Dry-run → Backup → History → Snapshot list → Restore → DR export

- history.svg (44 KB, ~6s): Backup History table with timestamps,
  duration, status, scope, volumes, snapshot IDs. Operations / "did the
  backups actually run?" sight.
- snapshot-list.svg (58 KB, ~8s): kopi-docka advanced snapshot list
  --snapshots — shows the tag-based organisation (every snapshot tagged
  with backup_id/type/unit so the wizard can group recipe + networks +
  volumes from one backup_id as a unit). Direct illustration of the
  Kopia-tag differentiator vs. archive-name-based tools.
- dr-export.svg (39 KB, ~8s): disaster-recovery export — shows the
  encrypted-ZIP bundle creation panel, including the "passphrase NOT
  stored in file" reminder.

All three captured from real testlab runs against the rclone+GDrive
backend (32 existing snapshots in the repo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:41:41 +00:00
TZERO78 3e900f951f docs(readme): swap restore demo from --yes to the interactive wizard
The --yes (CI/CD) flow worked but missed the actual story: the wizard
is where the Kopia-tag-based session grouping becomes visible (each
"backup session" in the list is one backup_id tying recipe + networks
+ volumes), and where the safety-backup-before-overwrite step earns
user trust.

Recording reconstructed from a real interactive session captured by
the user on the testlab. Compressed from ~2min real time to ~34s while
keeping every decision point visible: session selection, "Recreate
network?" prompt, "Restore volume NOW?" confirmation, target-directory
prompt, completion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:34:50 +00:00
TZERO78 b94e63763d docs(readme): add backup + restore demo SVGs to complete the user-flow
Follow-up to the doctor/dry-run demos — adds the two missing pieces of
the natural user journey:

- demo-backup.svg (23 KB, ~7s): real cold backup via SFTP. Shows
  per-stack container-stop → snapshot → restart cycle for both
  test-stack-redis and test-stack-nginx. Real exit 0 capture from the
  E2E SFTP test environment.
- demo-restore.svg (93 KB, ~10s): non-interactive 'restore --yes' run
  showing the restore-wizard panel, snapshot listing, file restore,
  network recreation, and container restart.

Order on the README is now: doctor → dry-run → backup → restore, matching
the actual user workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:21:44 +00:00
TZERO78 20a82793f2 docs(readme): add animated SVG demos for dry-run + doctor
HN/r/selfhosted-prep: visitors land on the README and want to see
"what does this actually do" in ten seconds, before reading any prose.
Two short animated SVGs (~7-9s each) embedded inline:

- demo-dry-run.svg: discovers the test-lab stacks (redis + nginx),
  prints the dry-run plan with estimates.
- demo-doctor.svg: dependency check, repository status, backend sanity,
  DR-readiness — the "this tool checks itself" angle.

SVGs are 56 KB / 88 KB, text-selectable when zoomed, rendered by GitHub
inline as raw.githubusercontent.com URLs (works on both GitHub and PyPI
README rendering — the URLs are absolute).

Generated with termtosvg from manually-constructed asciicast files
(the testlab session was too long to script with expect; output captured
once and timed-out for typing animation, ~8s each).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:16:31 +00:00
Markus b438dff0f9 Merge pull request #123 from TZERO78/docs/downloads-badge-pepy-tech
docs(readme): switch downloads badge from pypi/dm to pepy.tech
2026-05-25 09:53:59 +02:00
TZERO78 f56e22d33b docs(readme): switch downloads badge from pypi/dm to pepy.tech
img.shields.io/pypi/dm has been serving "rate limited by upstream
service" for some time — PyPIStats throttles shields.io's polling.
pepy.tech is the standard alternative: free, no rate-limit, ~5min
cache, accurate download counts (currently ~10k/month).

Link target also updated to pepy.tech/project/kopi-docka so clicking
the badge lands on the actual stats page, not just the PyPI project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:53:30 +00:00
Markus 6bdc7aceab Merge pull request #122 from TZERO78/docs/v7.6.2-docs-pass
release: v7.6.2 — docs pass (PyPI links, plan-slug cleanup, ARCHITECTURE refresh)
v7.6.2
2026-05-25 09:46:42 +02:00
TZERO78 8a823c71bc release: v7.6.2 — docs-only pass (Plan 0039)
PyPI link fix, plan-slug cleanup, CHANGELOG smoothing,
ARCHITECTURE.md refresh. No code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:43:44 +00:00
TZERO78 dbac615c72 docs(architecture): document helpers + repair pattern, refresh stale sections
Adds the 8 helper modules previously absent from ARCHITECTURE.md
(backend_helper, sudo_helper, metadata_reader, docker_run_builder,
file_operations, process_lock, system_utils, logging, ui_utils,
constants) — each with a one-paragraph purpose + signature table where
the API is non-trivial.

Adds a new "Repair pattern (rebuild_kopia_params)" section documenting
how advanced config repair-kopia-params dispatches generically through
BackendBase + MissingCredentialsError, with the extension point for
future per-backend repair logic.

Also: final Plan-XXX sweep — README's tested-backends table now uses
version anchors (v7.4.0 + v7.6.1) instead of slug refs.

Plan 0039 — Block D.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:42:23 +00:00
TZERO78 71992969b5 docs(changelog): smooth recent entries (v7.4.0–v7.6.1) to release-notes style
Recent entries had drifted into PR-description voice — verbose narrative
with multiple subsections per release. Tightened to a uniform shape:
one-line headline, "Why" (1-2 sentences), bulleted "Changes", optional
"Upgrade notes". CHANGELOG dropped from 2255 → 1993 lines, no
information lost that wasn't already covered by commit history / PRs.

Plan-XXX slugs in entry bodies replaced with version anchors; slugs in
the [7.3.0 – 7.3.8] cluster header are kept (legitimate release-notes
shorthand). Entries pre-v7.4.0 are untouched.

Plan 0039 — Block C.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:40:04 +00:00
TZERO78 b394cc5c55 docs: replace plan-slug refs with version anchors in user-facing docs
Internal planning artifacts ("Plan 0022", "Plan 0028") have no meaning to
end users reading the docs — rewrite to point at the release that
delivered the change (e.g. "since v7.3.0") instead. 8 occurrences across
FEATURES, CONFIGURATION, ARCHITECTURE, TROUBLESHOOTING.

Plan slugs are kept in CHANGELOG section headers where they serve as
release-notes shorthand (e.g. "v7.3.0 — Plan 0028 + post-release"), and
in the plan/ directory itself.

Plan 0039 — Block B.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:36:15 +00:00
TZERO78 730fd8e950 docs(readme): rewrite relative links to absolute GitHub URLs for PyPI
PyPI ships only the source tree listed in pyproject.toml; relative URLs
like docs/CONFIGURATION.md or LICENSE 404 on the PyPI project page. 31
occurrences rewritten to https://github.com/TZERO78/kopi-docka/blob/main/
(or /tree/main/ for directories). twine check PASSED.

Plan 0039 — Block A.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:34:53 +00:00
TZERO78 a2dbbbdfa0 docs(readme): clarify rclone — backend tested, GDrive perf is upstream
Splits the "what's tested" claim (rclone the Kopia backend, exercised
against a live test lab) from the "what's known broken" caveat (Google
Drive specifically has high small-file write overhead — upstream / rclone
limitation, not kopi-docka's). Links to the pinned tracking issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:33:32 +00:00
Markus 70ce014e18 Merge pull request #121 from TZERO78/fix/v7.6.1-sftp-canonical-params
fix(sftp): canonical Kopia params + backend-dispatched repair (v7.6.1, Plan 0038)
v7.6.1
2026-05-25 09:28:11 +02:00
TZERO78 f24baaef4c docs(readme): tested-backends table — rclone / tailscale / sftp marked
SFTP added after the v7.6.1 (Plan 0038) E2E verification: wizard →
`kopia repository create` → full `kopi-docka backup` against the
test-stack-nginx + test-stack-redis stacks via localhost SFTP, snapshot
persistence verified on the SFTP-backed Kopia repo. Rclone and Tailscale
already had real-world coverage from prior plans / live test lab.

Other backends (filesystem, S3, B2, Azure, GCS) are wired up and
unit-tested but I haven't driven an end-to-end backup cycle against
them — explicit about that so users know where community reports
would be most useful.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:07:51 +00:00
TZERO78 ff024a54d6 fix(sftp): canonical Kopia params + backend-dispatched repair (Plan 0038)
Plan 0038 — finishes the Plan 0029 story for the direct SFTP wizard,
which still shipped the broken `--path=user@host:path` shape and an
invalid `--sftp-port` flag until now. Verified locally: Kopia rejects
`--sftp-port` with `unknown long flag`; the bug never triggered because
port 22 (default) skipped the branch.

Single source of truth: new `helpers/backend_helper.py::build_sftp_kopia_params()`
+ `ensure_known_hosts()`. SFTP wizard, Tailscale wizard, and
`rebuild_kopia_params()` repair hook all feed through it.

`advanced config repair-kopia-params` is now backend-dispatched —
`BackendBase.rebuild_kopia_params(credentials)` is the entrypoint, each
backend overrides if it has a credentials-based rebuild path. New
`MissingCredentialsError` lets each backend declare its required keys
without the command hardcoding them. No `if backend == "sftp"` branches
left in the command.

SFTP wizard now also persists a `[credentials]` block so future configs
are repairable through the same flow as Tailscale.

Doctor sanity regex extended from `--path=…` to `--path[=\s]…` — the
v7.0.0–v7.6.0 direct-SFTP wizard wrote the space form, which the
Plan 0029 regex did not catch.

1244 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:54:24 +00:00
Markus 7f2da665f4 Merge pull request #120 from TZERO78/refactor/v7.6.0-sudo-helper-extraction
release: v7.6.0 — sudo_helper extraction (Plan 0037)
v7.6.0
2026-05-24 19:30:13 +02:00
TZERO78 30a6ad0898 fix: ci — drop unused os import + migrate test mocks to sudo_helper boundary
CI surfaced two issues my local lint missed:

1. After the sudo_helper migration, `os` is no longer used anywhere in
   `disaster_recovery_manager.py` (was only there for `os.environ.get`
   and `os.chown` — both now go through the helper). ruff F401 caught
   it. Dropped.

2. `test_dr_export.py::test_export_sets_ownership` patched
   `pwd.getpwnam` and `os.chown` directly — testing the OS-level
   implementation, not the new helper-mediated contract. Re-anchored
   the patches at the helper boundary
   (`kopi_docka.helpers.sudo_helper.os.chown`) and set SUDO_UID/SUDO_GID
   env-vars instead of mocking pwd. Same intent, new seam.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:28:45 +00:00
TZERO78 9278efc99a release: v7.6.0 — sudo_helper extraction (Plan 0037)
Minor bump (not patch) because the SUDO_USER validation regex now
applies at four sites that previously skipped it. Real-world impact:
zero — legitimate usernames pass through unchanged. But the contract
at those sites shifted, so SemVer says minor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:25:11 +00:00
TZERO78 58f2b44625 refactor: extract sudo_helper, replace 11 duplicated SUDO_USER patterns
Plan 0037. The SUDO_USER / SUDO_UID / SUDO_GID handling was duplicated
at 11 sites across 4 files with three different validation niveaus —
some checked SUDO_USER against a shell-injection regex, others didn't.

New module kopi_docka/helpers/sudo_helper.py exposes one typed API:

  @dataclass SudoUserInfo: name, uid, gid, home, invoked_with_sudo
  def get_sudo_user_info() -> SudoUserInfo
  def chown_to_sudo_user(path) -> None
  def find_in_sudo_user_home(relative) -> Optional[Path]
  def sudo_user_home_path(relative) -> Optional[Path]

All 11 sites migrated:

  - cores/disaster_recovery_manager.py (2 sites)
  - cores/restore_manager.py            (2 sites)
  - helpers/file_operations.py          (1 site)
  - backends/rclone.py                  (4 sites)

Side benefit: the four previously-unvalidated SUDO_USER reads now go
through the same shell-injection validation as the other sites, at
zero cost for legitimate usernames. backends/rclone.py no longer needs
`import os` either.

+18 unit tests in tests/unit/test_helpers/test_sudo_helper.py covering
under-sudo / no-sudo / shell-injection / path-traversal / garbage-UID /
chown success+failure / find/path-build variants. All 1234 prior tests
unchanged (behavioral parity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:25:04 +00:00
Markus 283a01d62a Merge pull request #119 from TZERO78/chore/v7.5.3-tz-aware-datetime-cleanup
release: v7.5.3 — tz-aware datetime cleanup
v7.5.3
2026-05-24 17:09:31 +02:00
TZERO78 52bbf33326 release: v7.5.3 — tz-aware datetime cleanup + Plan 0032 filed
Five naive datetime.now() ISO-string emit sites brought in line with
the v7.5.2 tz-aware-snapshot-tag convention. No user-visible change,
houskeeping after the read-only codebase scan.

Plan 0032 (proposed, nice-to-have) collects the non-trivial hygiene
items the scan also surfaced: subprocess timeout strategy for TAR-mode
and stdin-piping, Union[X,Y] modernization to X|Y, match-statement
adoption in the DR backend-dispatch chains, bare-except narrowing,
f-string-vs-%-style logging unification, and a pydantic<3 upper bound.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:07:47 +00:00
TZERO78 a9daf7de63 chore: tz-aware datetime cleanup (mop-up after v7.5.2)
Five remaining naive datetime.now().isoformat() call sites turned up
in a read-only codebase scan after v7.5.2 fixed the snapshot-tag
timestamps. All emit ISO strings that round-trip through
datetime.fromisoformat(); without a timezone they parse as naive and
break comparisons with tz-aware values downstream (same class of bug
that crashed the restore wizard in v7.5.2).

* cores/backup_manager.py:101 — BackupMetadata.timestamp goes into
  /backup/kopi-docka/metadata/*.json
* cores/backup_volume_handler.py:125, 207 — TAR-mode snapshot tags
  (legacy backup path)
* cores/disaster_recovery_manager.py:697 — created_at in DR-bundle
  recovery-info.json
* cores/disaster_recovery_manager.py:1069 — timestamp in DR-bundle
  backup-status.json

All five now emit datetime.now(timezone.utc).isoformat().

Pre-existing user-facing local-time strftime() calls used in filenames
(config-backup-*, restore-rollback tarballs, DR-bundle output
filenames) stay naive on purpose — they represent local wall-clock
time, not machine-parseable timestamps.

Two new regression tests cover the DR-bundle path; the existing v7.5.2
snapshot-timestamp tests cover the backup_manager paths. No user-
visible behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:07:37 +00:00
Markus 1a91ddd0de Merge pull request #118 from TZERO78/fix/v7.5.2-restore-tz-and-init-backend
release: v7.5.2 — restore tz-mix + repo init backend check
v7.5.2
2026-05-24 16:34:55 +02:00
TZERO78 047401da6d release: v7.5.2 — restore tz-mix + repo init backend check
Patch release for two field bugs surfaced after v7.5.1:

- restore wizard crashed on mixed naive/aware snapshot timestamps
- `advanced repo init` ignored kopia_params backend swaps and left
  Kopia connected to the previous backend, silently sending all
  subsequent backups to the wrong destination

Both fixes are non-disruptive: existing repos remain usable, no
config-format changes, no repo re-init required for the
timestamp-fix path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:33:03 +00:00
TZERO78 5196d0acda fix(repo): detect backend mismatch in initialize() and reconnect
Editing kopia_params in /etc/kopi-docka.json from one backend to
another (e.g. rclone -> sftp) followed by `advanced repo init`
silently kept Kopia talking to the old backend: initialize() only
checked is_connected() and treated any active connection as
"we're done", regardless of which backend it pointed to. The user
saw "Repository initialized successfully" while their new SFTP
target stayed empty.

Add _current_storage_type() that reads storage.type from Kopia's
connect-config and _expected_storage_type() that takes the first
token of kopia_params. In initialize(), compare both before the
is_connected() shortcut; on mismatch, log a warning and call
disconnect() so the subsequent create/connect actually retargets
to the new backend.

The check is intentionally limited to storage type — a path/host
swap inside the same backend still hits the shortcut and needs a
manual disconnect. That keeps the change minimal and focused on
the observed regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:32:56 +00:00
TZERO78 17cbb99fe3 fix(restore): normalize snapshot timestamps to tz-aware UTC
The restore wizard crashed with "can't compare offset-naive and
offset-aware datetimes" whenever the snapshot list contained one
tag-timestamp from pre-v7.5.2 backup_manager (naive, written via
datetime.now().isoformat()) and one fallback timestamp from the
restore_manager exception/default branch (aware, datetime.now(
timezone.utc)). sort() then crashed before any restore points
were shown.

Introduce _parse_snapshot_timestamp() in restore_manager that
always returns a tz-aware UTC datetime — naive legacy tags are
treated as UTC, parse errors fall back to now(utc). Use it from
both _find_restore_points() and _find_restore_points_for_machine().

In backup_manager, write new snapshot-tag timestamps and the
networks metadata backup_timestamp as datetime.now(timezone.utc).
isoformat() so future tags round-trip cleanly without ever needing
the legacy-as-UTC assumption.

Existing snapshots remain restorable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:32:47 +00:00
Markus ff376c36c8 Merge pull request #117 from TZERO78/fix/v7.5.1-dr-bundle-ssh-key-hygiene
release: v7.5.1 — DR-bundle SSH-key hygiene + recover.sh exit-fix
v7.5.1
2026-05-24 16:07:00 +02:00
TZERO78 006dd14d10 release: v7.5.1 — DR-bundle SSH-key hygiene + recover.sh exit-fix
Plan 0030. v7.5.0's E2E DR test surfaced two real issues:

1. recover.sh exited 1 for every SFTP/Tailscale bundle (legacy
   "Unsupported auto-connect" fall-through).
2. The SSH key was never in the bundle (correct by design — defense in
   depth, NIST SP 800-57) but nothing told the user that, so the gap
   was invisible until the day they actually needed it.

This release fixes (1), and addresses (2) with explicit visibility on
three surfaces: the export end-of-run panel, RECOVERY-INSTRUCTIONS.txt,
and a new doctor Section 9 "Disaster Recovery Readiness". Plus an
opt-in `--include-ssh-key` for users whose threat model is "bundle is
held at higher trust than the SSH key itself".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:05:11 +00:00
TZERO78 6ea858c836 docs(dr): document key separation + --include-ssh-key opt-in
New "What's NOT in the bundle (and why)" section in
docs/DISASTER_RECOVERY.md spells out:

* Which credentials live OUTSIDE the bundle per backend type (SSH key,
  AWS keys, B2 keys, Azure key, GCP service-account JSON).
* The defense-in-depth / NIST SP 800-57 key-separation rationale and a
  brief note that restic, Borg, Duplicity, rclone crypt all default to
  the same posture.
* Suggested storage for the SSH key (password-manager attachment,
  separate USB stick, GPG-symmetric, paper printout).
* The --include-ssh-key opt-in flag with its trade-off explicit.

Plan 0030 documentation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:05:05 +00:00
TZERO78 42472c68fc feat(doctor): Section 9 — Disaster Recovery Readiness
New doctor section that surfaces the SSH key status for SFTP /
Tailscale backends on every health check:

  9. Disaster Recovery Readiness
  ----------------------------------------
    Backend Type           sftp
    SSH Key                ✓ Found         /root/.ssh/kopi-docka_ed25519
    SSH Key SHA256                         71b25fd2b5bd…
                                           Record this for DR verification
    Known Hosts            ✓ Found         /root/.ssh/known_hosts

The point is to make the externally-held DR credential visible during
normal operation, not just at the moment of bundle export. If the key
disappears (renamed, perms broken, accidentally deleted) the user
finds out from the next routine doctor run rather than from a failed
recovery a year later. The SHA256 line is there so the user can record
the fingerprint and verify their externally-stored copy matches.

Silent for non-SFTP backends.

Plan 0030 / Phase 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:04:57 +00:00
TZERO78 a89c8d074e feat(dr): export panel + --include-ssh-key opt-in flag
Two additions on top of the recover.sh fix, both improving the trust-
boundary visibility of the DR bundle:

* After "Bundle Created", `disaster-recovery export` now prints a
  second panel — "⚠ Additional Secrets Required" — listing what the
  bundle does NOT contain and that the user must keep separately. For
  SFTP that's the SSH key path + SHA256 (so the user has a fingerprint
  to verify the externally-held copy on restore day). For cloud
  backends it's the credential variable names. Filesystem repos are
  silent.
* New `--include-ssh-key` flag for users who explicitly want the
  all-in-one bundle (e.g. when the bundle lives at a higher trust
  level than the SSH key itself — air-gapped storage, hardware vault).
  Default OFF. When passed, the flag triggers a red Security Trade-off
  warning panel and an explicit confirm prompt (`--yes` to skip in
  automation). When enabled, the key is embedded under ssh-key/* in
  the ZIP and recover.sh installs it at the expected target path with
  mode 600 before connecting. Panel + instructions + script all switch
  consistently to the embedded-key story.

Plan 0030 / Phases 3 + 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:04:51 +00:00
TZERO78 96b5d2c537 fix(dr): recover.sh handles SFTP backends without false-fail exit
Two related issues in the disaster-recovery script generator:

* The legacy generic `else` branch printed "Unsupported auto-connect
  for this repository scheme" and `exit 1` — even though the
  file-restore steps that ran before it had succeeded. Every SFTP /
  Tailscale recovery looked like a failure to the user.
* recover.sh had no real SFTP branch at all.

Now there's an explicit `elif repo_type == "sftp":` block that builds
a non-interactive `kopia repository connect sftp --path=… --host=…
--username=… --keyfile=… --known-hosts=…` from the connection info in
recovery-info.json. The block also guards on $KEYFILE being readable
before connecting — missing key prints a "install with mode 600 and
re-run" warning and exits 0 (since the file-restore portion already
succeeded). Unknown backends now also exit 0 with a manual-connect
hint instead of failing hard.

Also extends `_extract_repo_from_status` for SFTP to capture
port/username/keyfile/knownHostsFile (not just host/path) so the
generated connect call is complete. Adds module-level `sha256_file()`
helper used by the SHA256 fingerprints in instructions / doctor.

Plan 0030 / Phases 1 + 2. +11 unit tests covering the new branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:04:39 +00:00
Markus 5b13ad0e52 Merge pull request #116 from TZERO78/feat/v7.5.0-repair-kopia-params
release: v7.5.0 — advanced config repair-kopia-params
v7.5.0
2026-05-24 15:09:48 +02:00
TZERO78 b60fb72211 release: v7.5.0 — advanced config repair-kopia-params
Follow-up release on Plan 0029. v7.4.0 detected the broken
v7.0–v7.3.13 Tailscale-wizard kopia_params and handed out a sed
snippet to fix it. v7.5.0 turns that into a real command:

    sudo kopi-docka advanced config repair-kopia-params

The command rebuilds kopia_params from [credentials] atomically with
preview + confirmation. Idempotent, --dry-run, --yes flags. Doctor
Section 5.1 now points at this command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:08:00 +00:00
TZERO78 81fcb12f82 docs: update migration path to use repair-kopia-params
CONFIGURATION.md and TROUBLESHOOTING.md previously instructed
v7.0–v7.3.13 Tailscale users to copy/paste a sed line emitted by
doctor. v7.5.0 ships a real command for this; the docs now show that
instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:07:56 +00:00
TZERO78 996da9e264 refactor(doctor): point Backend Sanity at repair-kopia-params command
Section 5.1 used to print a generated `sudo sed -i 's#…#…#' /etc/...`
line. Replaced with a one-line pointer to the new
`advanced config repair-kopia-params` subcommand — easier to read, no
shell-escaping foot-guns, no copy/paste mistakes.

The `_build_sftp_migration_command` helper is removed entirely; the
two unit tests that exercised it are replaced by a single
TestBackendSanityHint case asserting doctor surfaces the new command
(and no longer emits `sudo sed -i`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:07:50 +00:00
TZERO78 7c4d7dd149 feat(config): advanced config repair-kopia-params
A one-command path to the v7.4.0 migration that v7.4.0 itself only
handed out as a sed snippet. Reads the still-correct [credentials]
section, rebuilds kopia_params in Kopia-SFTP's canonical
--path / --host / --username / --keyfile / --known-hosts shape, and
writes it back through the existing atomic Config.save().

Behavior:

- Shows an old-vs-new diff and prompts for confirmation.
- `--dry-run` to preview, `--yes` to skip the prompt.
- Idempotent: a second run says "already in the canonical shape".
- Refuses non-SFTP backends and configs missing remote_path / peer
  FQDN / ssh_key — those need the full wizard, not a parameter
  rebuild.

Follow-up to Plan 0029. The Kopia repository itself is untouched —
only the local config string changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:07:43 +00:00