Skip to content

Commit 0e64aeb

Browse files
Handle older Restic and harden workflow progress recovery
1 parent b8c91a9 commit 0e64aeb

9 files changed

Lines changed: 74 additions & 15 deletions

File tree

‎.github/workflows/linux-packages.yml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ jobs:
2525
components: clippy
2626
- name: Install build and smoke-test dependencies
2727
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf restic rpm libarchive-tools xvfb dbus-x11 libnotify-bin
28+
- name: Install checksum-pinned Restic for restore integration tests
29+
run: |
30+
curl -fsSL https://github.com/restic/restic/releases/download/v0.19.1/restic_0.19.1_linux_amd64.bz2 -o /tmp/restic-ci.bz2
31+
echo 'f415415624dcc452f2a02b8c33641791a8c6d6d3b65bbb3543fcf9a25151585c /tmp/restic-ci.bz2' | sha256sum -c -
32+
bzip2 -dk /tmp/restic-ci.bz2
33+
sudo install -m755 /tmp/restic-ci /usr/local/bin/restic
34+
restic version
2835
- run: npm ci
2936
- run: npm run check && npm test && npm run test:rust
3037
- run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
@@ -44,7 +51,7 @@ jobs:
4451
timeout-minutes: 35
4552
steps:
4653
- name: Install tools
47-
run: dnf install -y git nodejs npm rust cargo gcc gcc-c++ pkgconf-pkg-config openssl-devel webkit2gtk4.1-devel libappindicator-gtk3-devel librsvg2-devel rpm-build python3 binutils libarchive xorg-x11-server-Xvfb xorg-x11-xauth dbus-daemon shadow-utils libnotify
54+
run: dnf install -y git nodejs npm rust cargo gcc gcc-c++ pkgconf-pkg-config openssl-devel webkit2gtk4.1-devel libappindicator-gtk3-devel librsvg2-devel rpm-build python3 binutils bsdtar xorg-x11-server-Xvfb xorg-x11-xauth dbus-daemon shadow-utils libnotify restic
4855
- uses: actions/checkout@v4
4956
- uses: actions/setup-node@v4
5057
with:

‎docs/installation.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,5 @@ The browser preview uses sample repositories and configurations. It cannot run G
7979
After installation, open **Settings → System integrations**. Command Center can detect an existing dotfiles machine configuration, backup helpers, and Ghostty/Fastfetch source paths. Review the detected paths and save your settings.
8080

8181
Git, Home Manager, Restic, Ghostty, Fastfetch, and backup helpers are used when installed and configured. They are not all required to open the app. See the [user guide](user-guide.md#connect-your-setup) for each integration.
82+
83+
File restores require **Restic 0.17 or newer** for its no-overwrite protection; Command Center checks support before preparing a restore. Some distributions ship an older Restic independently of Command Center.

‎docs/release-notes.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Git file diffs, branches, stashes, branch publishing, service controls and audit
1515

1616
### Linux downloads
1717

18-
The `.deb` and `.rpm` assets target **x86_64** and require **glibc 2.39+, GTK 3, WebKitGTK 4.1, and the platform's AppIndicator library**. Notifications additionally use `notify-send`. Install your downloaded package with `sudo apt install ./command-center_0.4.0_amd64.deb` or `sudo dnf install ./command-center-0.4.0-1.x86_64.rpm` on a compatible distribution.
18+
The `.deb` and `.rpm` assets target **x86_64** and require **glibc 2.39+, GTK 3, WebKitGTK 4.1, and the platform's AppIndicator library**. Notifications additionally use `notify-send`. File restores require Restic 0.17+; older versions receive a compatibility error before a restore starts. Install your downloaded package with `sudo apt install ./command-center_0.4.0_amd64.deb` or `sudo dnf install ./command-center-0.4.0-1.x86_64.rpm` on a compatible distribution.
1919

2020
Download `SHA256SUMS` beside the package and run `sha256sum --check --ignore-missing SHA256SUMS`.
2121

‎packaging/aur/PKGBUILD‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ prepare() {
1717
}
1818
build() {
1919
cd "$srcdir/command-center"
20-
npm run desktop:build -- --locked
20+
npm run desktop:build -- -- --locked
2121
}
2222
package() {
2323
cd "$srcdir/command-center"

‎src-tauri/src/integrations.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,10 @@ pub fn build_plan(r: &Request, s: &Settings) -> Result<Plan, String> {
378378
plan.explanation = "Check repository structure and metadata. This does not perform a full read of every data block.".into();
379379
}
380380
"restore" => {
381+
let (ok, help, error) = p::output(Command::new("restic").args(["restore", "--help"]), 15)?;
382+
if !ok || !help.contains("--overwrite") || !help.contains("--verify") {
383+
return Err(format!("File restore requires Restic 0.17 or newer with --overwrite and --verify support. Update Restic before restoring. {error}"));
384+
}
381385
snapshot_id(&r.snapshot)?;
382386
let target = p::expand(&r.target)?;
383387
validate_restore_target(&target)?;

‎src/operations-model.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,33 @@ export function timelineFilter(rows, query, category) {
9595
),
9696
);
9797
}
98+
99+
export function restoreRun(value) {
100+
if (
101+
!value ||
102+
typeof value.name !== "string" ||
103+
!Array.isArray(value.rows) ||
104+
!value.rows.length ||
105+
value.rows.length > 30 ||
106+
!Number.isInteger(value.index) ||
107+
value.index < 0 ||
108+
value.index > value.rows.length ||
109+
value.rows.some(
110+
(row) =>
111+
typeof row.name !== "string" ||
112+
typeof row.status !== "string" ||
113+
!row.request ||
114+
!stepTypes[row.request.action],
115+
)
116+
)
117+
return null;
118+
if (value.index === value.rows.length && value.status !== "succeeded")
119+
return null;
120+
return {
121+
...value,
122+
jobId: null,
123+
status: ["succeeded", "stopped"].includes(value.status)
124+
? value.status
125+
: "interrupted",
126+
};
127+
}

‎src/operations.js‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
stepTypes,
55
toolFolderItems,
66
advanceRun,
7+
restoreRun,
78
timelineFilter,
89
} from "./operations-model.js";
910
export function createOperations({
@@ -69,13 +70,9 @@ export function createOperations({
6970
}
7071
};
7172
try {
72-
current = JSON.parse(
73-
localStorage.getItem("command-center-workflow-run") || "null",
73+
current = restoreRun(
74+
JSON.parse(localStorage.getItem("command-center-workflow-run") || "null"),
7475
);
75-
if (current && !["succeeded", "stopped"].includes(current.status)) {
76-
current.status = "interrupted";
77-
current.jobId = null;
78-
}
7976
} catch {
8077
current = null;
8178
}
@@ -677,12 +674,14 @@ export function createOperations({
677674
$("#notification-form").addEventListener("submit", async (ev) => {
678675
ev.preventDefault();
679676
try {
677+
const preferences = {};
678+
for (const key of ["enabled", "failures", "completions", "health"])
679+
preferences[key] = $("#notice-" + key).checked;
680+
preferences.quietStart = Number($("#notice-start").value);
681+
preferences.quietEnd = Number($("#notice-end").value);
680682
await ensure();
681683
const next = structuredClone(collection);
682-
for (const key of ["enabled", "failures", "completions", "health"])
683-
next.notifications[key] = $("#notice-" + key).checked;
684-
next.notifications.quietStart = Number($("#notice-start").value);
685-
next.notifications.quietEnd = Number($("#notice-end").value);
684+
next.notifications = preferences;
686685
await save(next);
687686
$("#notice-status").textContent = "Notification preferences saved.";
688687
} catch (err) {

‎src/styles.css‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/operations.test.mjs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import test from "node:test";
22
import assert from "node:assert/strict";
33
import {
44
advanceRun,
5+
restoreRun,
56
toolFolderItems,
67
timelineFilter,
78
weeklyRecipe,
@@ -75,3 +76,19 @@ test("starter workflow checks readiness before backup and contains no unattended
7576
["backup-ready", "backup", "restic-check", "health-check"],
7677
);
7778
});
79+
80+
test("corrupted progress is ignored and unfinished work never resumes automatically", () => {
81+
assert.equal(restoreRun({ name: "Broken", rows: [], index: 0 }), null);
82+
assert.equal(restoreRun({ name: "Broken", rows: [{}], index: 0 }), null);
83+
const restored = restoreRun({
84+
name: "Backup",
85+
index: 0,
86+
status: "running",
87+
jobId: "old",
88+
rows: [
89+
{ name: "Backup", request: { action: "backup" }, status: "running" },
90+
],
91+
});
92+
assert.equal(restored.status, "interrupted");
93+
assert.equal(restored.jobId, null);
94+
});

0 commit comments

Comments
 (0)