Files
OPNexus/frontend/components/UpdateBanner.tsx
T
Julian Boldt c662a58426
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
Release v1.15.0
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.
2026-10-10 00:24:01 +02:00

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>
);
}