Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
Updates aus dem Webinterface. Auf Klick, mit Fortschritt und automatischem Rollback; ein eigener, optionaler Container erledigt die Arbeit. Ende-zu-Ende mit echten Images getestet, siehe Handbuch „Update aus dem Webinterface (optional)“: - **Updates aus dem Webinterface (optional):** Einstellungen › „OPNexus aktualisieren“ zeigt die neueste Version samt Release-Notes und aktualisiert auf Klick — mit Fortschrittsanzeige, automatischem Rollback bei einem fehlgeschlagenen Update und „Auf Vorversion zurücksetzen“ (mit Hinweis, ob dabei Daten verloren gehen). Dahinter arbeitet ein eigener, kleiner **Updater-Container** (`updater/`, Compose-Profil `updater`, mit `COMPOSE_PROFILES=updater` in `.env` aktivieren), der dieselbe Logik wie `opnexus update` ausführt. Er ist standardmäßig **aus** und der einzige Container mit Docker-Socket; das Backend bekommt ihn nie. Der Updater hat keinen veröffentlichten Port, ist nur mit einem selbst erzeugten Token ansprechbar und kann ausschließlich „aktualisieren auf die vom Release-Feed gemeldete neueste stabile Version“, „zurückrollen“ und „Status“. Hauptversions-Sprünge bleiben der Kommandozeile vorbehalten. Anfragen stehen im Audit-Log (neuer Typ „OPNexus-System“). Gilt nur für Release-Images. - `opnexus` (jetzt in `updater/opnexus`, `scripts/opnexus` ist ein Symlink): `status --porcelain`, Exit-Code 3 für „fehlgeschlagen und sauber zurückgerollt“. Release-Images und -Bundle enthalten zusätzlich `opnexus-updater`. - Der Banner „Neue Version verfügbar“ verlinkt auf den neuen Abschnitt in den Einstellungen.
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import { api, VersionCheck } from "@/lib/api";
|
|
import { useLanguage } from "@/components/LanguageProvider";
|
|
import { useReadOnly } from "@/lib/useReadOnly";
|
|
import { ArrowUpCircle, X } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
|
|
const DISMISSED_KEY = "opnexus-update-dismissed";
|
|
|
|
function readDismissed(): string | null {
|
|
try {
|
|
return localStorage.getItem(DISMISSED_KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* "Neue Version verfügbar"-Hinweis. Rein informativ: OPNexus aktualisiert sich
|
|
* nie selbst, das macht `opnexus update` auf dem Host. Erscheint nur für
|
|
* Administratoren (Nur-Lesen-Konten können nichts damit anfangen), nur wenn
|
|
* der Server den Check aktiviert hat (UPDATE_CHECK_URL), und pro Version nur
|
|
* bis zum Wegklicken — eine noch neuere Version zeigt ihn wieder.
|
|
*/
|
|
export function UpdateBanner() {
|
|
const { t } = useLanguage();
|
|
const readOnly = useReadOnly();
|
|
const [check, setCheck] = useState<VersionCheck | null>(null);
|
|
const [dismissed, setDismissed] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
setDismissed(readDismissed());
|
|
let active = true;
|
|
api
|
|
.versionCheck()
|
|
.then((result) => {
|
|
if (active) setCheck(result);
|
|
})
|
|
.catch(() => {
|
|
// Ein nicht erreichbarer Check ist kein Fehler, der den Nutzer stören soll.
|
|
});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, []);
|
|
|
|
if (readOnly || !check?.update_available || !check.latest || dismissed === check.latest) return null;
|
|
|
|
const releaseUrl = check.release_url && /^https?:\/\//.test(check.release_url) ? check.release_url : null;
|
|
|
|
function dismiss() {
|
|
try {
|
|
localStorage.setItem(DISMISSED_KEY, check!.latest!);
|
|
} catch {
|
|
// Ohne Storage bleibt es bei "bis zum nächsten Laden ausgeblendet".
|
|
}
|
|
setDismissed(check!.latest);
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto max-w-6xl px-4 pt-4 sm:px-6">
|
|
<div role="status" className="flex items-start gap-3 rounded-lg border border-sky-100 bg-sky-50 p-3 text-sm text-sky-800">
|
|
<ArrowUpCircle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
|
|
<div className="flex-1">
|
|
<p className="font-medium">{t("updateBanner.title", { latest: check.latest, current: check.current })}</p>
|
|
<p className="text-xs">
|
|
{t("updateBanner.how")}{" "}
|
|
<a href="/settings#opnexus-update" className="font-medium underline">
|
|
{t("updateBanner.settings")}
|
|
</a>
|
|
{releaseUrl && (
|
|
<>
|
|
{" "}
|
|
<a href={releaseUrl} target="_blank" rel="noopener noreferrer" className="font-medium underline">
|
|
{t("updateBanner.notes")}
|
|
</a>
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
<button onClick={dismiss} aria-label={t("updateBanner.dismiss")} className="shrink-0 rounded p-0.5 hover:bg-black/5">
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|