setForm({ ...form, name: event.target.value })} />
@@ -350,6 +352,8 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
{error &&
{error}
}
+ {isLoading &&
{t("monitorLinks.checking")}
}
+ {loadError &&
{t("monitorLinks.loadFailed")}
}
{data?.links.length === 0 &&
{t("monitorLinks.empty")}
}
{data?.links.map(link => {
const own = link.manual || link.custom_url
@@ -378,8 +382,8 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
{t("monitorLinks.thisNode")}
: isEditing
- ?
-
+
setAddresses({ ...addresses, [link.key]: event.target.value })} />
-
{store.error || t("pbs.capacityUnavailable")}
+
}
const tone = usageTone(percent)
@@ -62,10 +64,11 @@ export function DatastoreUsageRow({ store, compact = false }: { store: PBSDatast
// Same layout as the "Proxmox storage" cards of the PVE Monitor.
export function PBSDatastoreCard({ store }: { store: PBSDatastore }) {
const t = useT()
+ const { language } = useI18n()
const percent = datastorePercent(store)
const tone = percent == null ? null : usageTone(percent)
const fullDate = store["estimated-full-date"]
- const type = store["backend-type"] || "datastore"
+ const type = store["backend-type"] || t("pbs.datastoreType")
return
@@ -99,13 +102,13 @@ export function PBSDatastoreCard({ store }: { store: PBSDatastore }) {
) : (
-
{store.error || t("storage.capacityNotReported")}
+
)}
{(store.path || fullDate !== undefined) &&
{store.path && {store.path}}
{fullDate !== undefined &&
{t("pbs.estimatedFull")}:
- {fullDate > 0 ? new Date(fullDate * 1000).toLocaleDateString() : t("pbs.estimatedFullUnknown")}
+ {fullDate > 0 ? new Date(fullDate * 1000).toLocaleDateString(language) : t("pbs.estimatedFullUnknown")}
}
}
diff --git a/AppImage/components/pbs-panel.tsx b/AppImage/components/pbs-panel.tsx
index 5ea6856d..5dff026e 100644
--- a/AppImage/components/pbs-panel.tsx
+++ b/AppImage/components/pbs-panel.tsx
@@ -14,8 +14,10 @@ import { Label } from "./ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import { PBSDatastoreCard, type PBSDatastore } from "./pbs-datastore-usage"
+import { PBSProblemMessage } from "./pbs-problem-message"
+import { pbsDiagnostic, pbsResultText, pbsServiceState, pbsTaskType, formatPBSDuration } from "../lib/pbs-messages"
import { fetchApi } from "../lib/api-config"
-import { useT } from "../lib/i18n/provider"
+import { useT, useI18n } from "../lib/i18n/provider"
interface Connection { id: string; name: string; url: string; token_id: string; fingerprint: string; has_token: boolean }
interface Detected { name: string; url: string; fingerprint: string }
@@ -26,6 +28,7 @@ interface PBSStatus {
name: string; version: string | null; fetched_at: number; services: Record
;
datastores: PBSDatastore[] | null; tasks: Task[] | null;
sync: Job[] | null; verify: Job[] | null; prune: Job[] | null; errors: Record;
+ error_details?: Record }>;
}
const emptyForm = { id: "", name: "", url: "", token_id: "", fingerprint: "", token_secret: "", username: "", password: "" }
@@ -46,9 +49,10 @@ const TASK_ICONS: Record = {
termproxy: Terminal, dircreate: Database, "create-datastore": Database,
}
-// PBS reports "OK", "WARNINGS: n" or the error text; a task without one is still running.
+// PBS reports "unknown", "OK", "WARNINGS: n" or an error; no state is still running.
function taskState(task: Task): keyof typeof badgeTone {
if (!task.status) return task.endtime ? "muted" : "running"
+ if (task.status === "unknown") return "muted"
if (task.status === "OK") return "ok"
if (task.status.startsWith("WARNINGS")) return "warn"
return "error"
@@ -56,27 +60,19 @@ function taskState(task: Task): keyof typeof badgeTone {
// PBS reports "OK", "WARNINGS: n" or the error text of the last run.
function jobState(state?: string): keyof typeof badgeTone {
- if (!state) return "muted"
+ if (!state || state === "unknown") return "muted"
if (state.toLowerCase() === "ok") return "ok"
if (state.toLowerCase().startsWith("warn")) return "warn"
return "error"
}
-function formatDuration(seconds: number) {
- if (!Number.isFinite(seconds) || seconds < 0) return "—"
- if (seconds < 60) return `${Math.round(seconds)}s`
- const minutes = Math.floor(seconds / 60)
- if (minutes < 60) return `${minutes}m ${Math.round(seconds % 60)}s`
- const hours = Math.floor(minutes / 60)
- return hours < 24 ? `${hours}h ${minutes % 60}m` : `${Math.floor(hours / 24)}d ${hours % 24}h`
-}
-
// `localOnly` is the PBS host's own Monitor: no connection picker, the
// header Refresh already reloads the page data. Its tabs show one `view` each.
export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: boolean; view?: "all" | "datastores" | "tasks" } = {}) {
const showStores = view !== "tasks"
const showTasks = view !== "datastores"
const t = useT()
+ const { language } = useI18n()
const { data: connections, error: connectionError, mutate: reloadConnections } = useSWR(
"/api/pbs/servers", url => fetchApi(url), { revalidateOnFocus: false })
const [selected, setSelected] = useState("")
@@ -85,7 +81,8 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
const [method, setMethod] = useState("password")
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState("")
- const [error, setError] = useState("")
+ const [error, setError] = useState(null)
+ const [errorFallback, setErrorFallback] = useState("pbs.saveFailed")
const { data, error: statusError, isLoading, mutate } = useSWR(
selected ? `/api/pbs/status?server=${encodeURIComponent(selected)}` : null,
url => fetchApi(url), { refreshInterval: 30000, revalidateOnFocus: false })
@@ -102,7 +99,9 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
}
const save = async () => {
+ setErrorFallback("pbs.saveFailed")
setBusy(true); setError(""); setMessage("")
+ let saved = false
try {
// A password is sent once to create a read-only token; only the token is kept.
const { username, password, ...connection } = form
@@ -113,22 +112,26 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
: await fetchApi<{ id: string }>("/api/pbs/servers", { method: "POST", body: JSON.stringify({
...connection, id: form.id || undefined, token_secret: form.token_secret || undefined,
}) })
+ saved = true
await reloadConnections()
setSelected(result.id)
setForm(emptyForm)
setManage(false)
- } catch (err) { setError(err instanceof Error ? err.message : t("pbs.saveFailed")) }
+ } catch (err) { setError(saved ? { body: { code: "pbs_saved_refresh_failed" }, message: pbsDiagnostic(err) } : err) }
finally { setBusy(false) }
}
const remove = async () => {
if (!form.id || !window.confirm(t("pbs.removeConfirm"))) return
+ setErrorFallback("pbs.removeFailed")
setBusy(true); setError(""); setMessage("")
+ let removed = false
try {
await fetchApi(`/api/pbs/servers/${encodeURIComponent(form.id)}`, { method: "DELETE" })
+ removed = true
setForm(emptyForm)
await reloadConnections()
- } catch (err) { setError(err instanceof Error ? err.message : t("pbs.saveFailed")) }
+ } catch (err) { setError(removed ? { body: { code: "pbs_removed_refresh_failed" }, message: pbsDiagnostic(err) } : err) }
finally { setBusy(false) }
}
const now = Date.now() / 1000
@@ -150,8 +153,9 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
{t("pbs.manage")}
- {String((connectionError || statusError)?.message || t("pbs.loadFailed"))}