Status LEDs, image model selection, backup restore, provider test, monthly versions
- Sidebar: three status LEDs (server, models, MCP) with an overview popover (/api/status) - Images: preview stage, purpose-based model choice and reference images (Nano Banana Pro edit refines the chosen preview) - Settings: restore a stored or uploaded backup (staged, applied on restart) - Provider form: test connection; saving or reloading re-checks the status at once - opencode Zen/Go: send x-opencode-session with a stable id per conversation - Releases are named vYYYYMM-NNNNN; scripts/release.sh numbers, tags, builds and publishes - Fix: backups made in the same second no longer overwrite each other Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49d404eec7
commit
1f69d63b09
35 files changed
+1156
-88
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { get, patch, post } from "../../api";
|
||||
import { toast } from "../../dialogs";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { api, get, patch, post } from "../../api";
|
||||
import { confirmDialog, toast } from "../../dialogs";
|
||||
import { useFetch } from "../../hooks";
|
||||
import { fmtDate, useT } from "../../i18n";
|
||||
import { formatSize } from "../FileFigure";
|
||||
@@ -90,6 +90,32 @@ export function Backups() {
|
||||
}
|
||||
};
|
||||
|
||||
// Restore: the server stages the backup, restarts and applies it before opening the database.
|
||||
const fileInput = useRef<HTMLInputElement>(null);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const restore = async (source: { name: string } | { file: File }) => {
|
||||
const label = "name" in source ? source.name : source.file.name;
|
||||
const ok = await confirmDialog({ title: t("backup.restoreTitle"), message: t("backup.restoreConfirm", { file: label }), confirmLabel: t("backup.restore"), danger: true });
|
||||
if (!ok) return;
|
||||
setRestoring(true);
|
||||
try {
|
||||
if ("name" in source) await post(`/backups/${encodeURIComponent(source.name)}/restore`);
|
||||
else await api("/backups/restore", { method: "POST", body: source.file, headers: { "Content-Type": "application/zip" } });
|
||||
toast(t("backup.restarting"), "info");
|
||||
// Wait until the server is gone and back, then reload with the restored data.
|
||||
await new Promise((r) => setTimeout(r, 2500));
|
||||
for (let i = 0; i < 120; i++) {
|
||||
if (await fetch("/api/health").then((r) => r.ok).catch(() => false)) return location.reload();
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
toast(t("backup.notBack"));
|
||||
} catch (e: any) {
|
||||
toast(e.message);
|
||||
} finally {
|
||||
setRestoring(false);
|
||||
}
|
||||
};
|
||||
|
||||
const s3 = (k: keyof S3, label: string, placeholder = "", type = "text") =>
|
||||
form && (
|
||||
<label className="field">
|
||||
@@ -111,6 +137,20 @@ export function Backups() {
|
||||
<a className="button small" href="/api/backup">
|
||||
<Icon name="download" /> {t("audit.download")}
|
||||
</a>
|
||||
<button className="small" onClick={() => fileInput.current?.click()} disabled={restoring}>
|
||||
<Icon name="upload" /> {restoring ? t("backup.restoring") : t("backup.restoreFile")}
|
||||
</button>
|
||||
<input
|
||||
ref={fileInput}
|
||||
type="file"
|
||||
accept=".zip,application/zip"
|
||||
hidden
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = "";
|
||||
if (file) void restore({ file });
|
||||
}}
|
||||
/>
|
||||
<button className="small primary" onClick={runNow} disabled={busy}>
|
||||
{busy ? t("backup.running") : t("backup.now")}
|
||||
</button>
|
||||
@@ -176,12 +216,17 @@ export function Backups() {
|
||||
{data && data.files.length > 0 && (
|
||||
<div className="backup-list">
|
||||
{data.files.map((f) => (
|
||||
<a key={f.name} href={`/api/backups/${encodeURIComponent(f.name)}`} className="backup-file">
|
||||
<div key={f.name} className="backup-file">
|
||||
<Icon name="file" />
|
||||
<span className="grow">{f.name}</span>
|
||||
<span className="hint">{formatSize(f.size)}</span>
|
||||
<Icon name="download" />
|
||||
</a>
|
||||
<button className="small" title={t("backup.restore")} onClick={() => void restore({ name: f.name })} disabled={restoring}>
|
||||
<Icon name="undo" />
|
||||
</button>
|
||||
<a className="button small" title={t("audit.download")} href={`/api/backups/${encodeURIComponent(f.name)}`}>
|
||||
<Icon name="download" />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user