While auditing real-world Bambuddy backup repos on GitHub I found
several left public. That's a serious leak: the settings backup only
filters bambu_cloud_token and auth_secret_key, so mqtt_username,
mqtt_password, ha_token, prometheus_token, bambu_cloud_email,
external_url, and the printer access codes (via K-profiles) were going
to whatever visibility the user picked.
Hard guard at every save and re-checked on every push:
- POST /github-backup/config and PATCH /github-backup/config (when URL,
token, or provider changes) run a connection test internally and
return 400 unless is_private comes back True.
- run_backup() re-checks before each scheduled or manual push, so a
repository that flipped from private to public gets a clear
"Backup aborted: the target repository is no longer private" failure.
Each provider's test_connection now returns is_private (GitHub /
Gitea / Forgejo read data.private, GitLab reads visibility=="private";
"internal" is treated as non-private). None means "couldn't determine"
and is also rejected -- safer to fail closed.
Frontend renders visibility inline on Test Connection: green check when
private, red warning panel listing every credential at risk when public,
yellow when unknown.
---
ui(github-backup): show save-failure messages inline on the card
The new "repository is not private" rejection message is ~250 characters
listing every credential the backup carries (MQTT password, HA token,
Prometheus token, Bambu Cloud email, printer access codes), which clips
badly in a toast.
Both the initial-setup save and the debounced autosave now stash the
backend's error message into a saveError state and render it as a red
inline banner above the test-result block, with whitespace-pre-wrap so
the full message stays readable. The banner clears on success, on the
next save attempt, and when the user starts editing URL / token / provider
-- the three fields whose changes invalidate the privacy check -- so it
doesn't linger after the user has already addressed the cause.
Short success toasts (Settings saved, Token updated, Backup enabled) are
unchanged.
feat(#1239): first cut at Gitea backups silently failing after 1st run
feat(#1239): Added Token Scope for Forgejo edge case. Also included: test coverage for fixes
Subsequent backups against Gitea 1.24+ failed with the opaque
"Backup failed: 'tree'" message after the initial-backup fix landed in
7ee89b56. Root cause: Gitea's GET /repos/{owner}/{repo}/git/commits/{sha}
returns the wrapped Commit schema where the tree lives at
data["commit"]["tree"]["sha"], whereas GitHub's same-named Git Database
endpoint returns the unwrapped GitCommit schema with tree at the top
level. The bare commit_response.json()["tree"]["sha"] lookup at
gitea.py:109 raised KeyError: 'tree' and the broad except in push_files
surfaced it as the opaque "Backup failed: 'tree'" string — masking the
real shape mismatch.
Adds a _commit_tree_sha() helper that tries the flat shape first
(GitHub-compatible / older Gitea) and falls back to the wrapped shape
(Gitea 1.24+, Forgejo). Returns None on truly malformed responses;
push_files maps that to a clear "Failed to extract tree SHA from commit
response" instead of leaking a KeyError repr. Keeps the existing-files
diff working on both shapes so subsequent backups don't re-upload every
blob — preferred over the .get()-and-skip approach which would have
required also dropping base_tree from the tree POST and re-uploading
unchanged files on every backup.
Two interacting bugs in the Gitea/Forgejo backend, both inherited from
GitHubBackend because PR #1160 assumed Gitea's Git Data API was fully
GitHub-compatible. It isn't, on two specific points:
1. List-shaped ref response. Gitea/Forgejo's
GET /api/v1/repos/{owner}/{repo}/git/refs/heads/{branch} returns a
GET /api/v1/repos/{owner}/{repo}/git/refs/heads/{branch} returns a
list of matching refs even when only one matches; GitHub returns a
single object. The inherited push paths did
ref_response.json()["object"]["sha"] and crashed with
"list indices must be integers or slices, not str" against any
populated Gitea repo.
2. Empty-repo writes refused. GitHub accepts blob/tree/commit POSTs
against a brand-new empty repo and creates the initial commit
implicitly. Gitea refuses every blob POST with 404 until the repo
has at least one commit, so _create_initial_commit silently failed:
blobs returned 404, tree_items stayed empty, the tree POST then
also 404'd ("Failed to create tree").
Fix lives entirely in GiteaBackend — github.py is untouched so the
proven GitHub path takes zero risk. GiteaBackend now overrides
push_files, _create_branch_and_push, and _create_initial_commit:
- _ref_sha() helper accepts both list and dict shapes; called at the
two SHA extraction sites in push_files and _create_branch_and_push.
- _create_initial_commit posts to Gitea's Contents API
(POST /api/v1/repos/{owner}/{repo}/contents with a files array plus
branch + new_branch) which seeds the initial commit + branch in
one transaction and is documented to work on empty repos.
ForgejoBackend extends GiteaBackend with no overrides and inherits
both fixes; tests pin that.