Fix data archives and restores failing on their own lock

startDataBackupRun already holds the backup run lock, but the archive
(createDataBackup) and restoreDataBackup took it again, so every run
failed a few seconds in with "Another backup or restore is already
running". The writer and the restore now take no lock of their own.

The run's notice (archiving, restoring, or why it failed) moves to its
own line under the actions row so the buttons don't shift, and archiving
reads in gray rather than the warning colour.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-25 09:24:55 +01:00
co-authored by Claude Opus 5.5
parent 410d05e0b1
commit b9a49ee09c
11 changed files with 141 additions and 117 deletions
@@ -14,6 +14,10 @@ export type DataBackupRunState = {
* Follows a run a request just started until it ends.
*/
track: (run: DataBackupRun) => void;
/**
* Shows why a run couldn't start (or clears it with `undefined`).
*/
report: (error?: string) => void;
};
/**
@@ -23,4 +27,5 @@ export type DataBackupRunState = {
*/
export const DataBackupRunContext = React.createContext<DataBackupRunState>({
track: () => {},
report: () => {},
});
@@ -94,5 +94,6 @@ export default function useDataBackupRun(): DataBackupRunState {
setError(undefined);
setRun(next);
},
report: setError,
};
}
@@ -1,6 +1,5 @@
import { Archive, Upload } from "lucide-react";
import Button from "@moduletrace/twui/layout/Button";
import Tag from "@moduletrace/twui/elements/Tag";
import Row from "@moduletrace/twui/layout/Row";
import useStatus from "@moduletrace/twui/hooks/useStatus";
import createDataBackup from "../(functions)/create-data-backup";
@@ -10,18 +9,17 @@ import { useContext, useState } from "react";
/**
* The data archive list's two ways in: make a fresh archive, or restore
* one from a file. Also says how the archive or restore running now is
* going, and why the last one failed.
* one from a file. How a run is going, and why one failed, is shown under
* the row by `DataBackupRunNotice`, so the buttons never move.
*/
export default function DataBackupListActions() {
const { loading, setLoading, status, setStatus } = useStatus();
const { run, error, track } = useContext(DataBackupRunContext);
const { loading, setLoading } = useStatus();
const { run, track, report } = useContext(DataBackupRunContext);
const [uploading, setUploading] = useState(false);
const archiving = run?.kind == "archive";
const restoring = run?.kind == "restore";
const create_backup = async () => {
setStatus(undefined);
report(undefined);
setLoading(true);
const res = await createDataBackup();
@@ -34,20 +32,12 @@ export default function DataBackupListActions() {
}
if (res.msg) {
setStatus({ error: true, msg: res.msg });
report(res.msg);
}
};
return (
<Row className="gap-2! flex-nowrap">
{status?.error && <Tag color="error">{status.msg}</Tag>}
{!status?.error && error && <Tag color="error">{error}</Tag>}
{archiving && (
<Tag color="warning">Archiving, this can take a while...</Tag>
)}
{restoring && (
<Tag color="warning">Restoring, the app restarts soon...</Tag>
)}
<Button
title="Restore from an archive file"
variant="outlined"
@@ -0,0 +1,26 @@
import { useContext } from "react";
import Tag from "@moduletrace/twui/elements/Tag";
import { DataBackupRunContext } from "../(data)/data-backup-run-context";
/**
* One line under the data archives' actions: the archive or restore
* running now, else why the last one failed. Nothing when neither, so the
* actions row keeps its layout either way.
*/
export default function DataBackupRunNotice() {
const { run, error } = useContext(DataBackupRunContext);
if (run?.kind == "archive") {
return <Tag color="gray">Archiving, this can take a while...</Tag>;
}
if (run?.kind == "restore") {
return <Tag color="warning">Restoring, the app restarts soon...</Tag>;
}
if (error) {
return <Tag color="error">{error}</Tag>;
}
return null;
}
@@ -4,6 +4,7 @@ import Span from "@moduletrace/twui/layout/Span";
import SettingsGroup from "../../(partials)/settings-group";
import { AppContext } from "@/src/pages/__root";
import DataBackupListActions from "../(partials)/data-backup-list-actions";
import DataBackupRunNotice from "../(partials)/data-backup-run-notice";
import DataBackupsTable from "../(partials)/data-backups-table";
import { DataBackupRunContext } from "../(data)/data-backup-run-context";
import useDataBackupRun from "../(hooks)/use-data-backup-run";
@@ -33,6 +34,7 @@ export default function DataBackupsSection() {
</Span>
<DataBackupListActions />
</Row>
<DataBackupRunNotice />
{backups[0] ? (
<DataBackupsTable backups={backups} />
) : (