feat(v5): Update sichtbar machen — Meldung, Knopf und Changelog
Ampel / ampel (push) Successful in 1m33s

Der Commander: "irgendwie fehlt in rippy selbst der Button 'Auf updates
pruefen' oder auch direkt beim start ne meldung 'ey jo update
verfuegbar' - waere noch cool wenn das kommt inkl. changelog".

Zu Recht. Der Kanal arbeitete korrekt, war aber UNSICHTBAR: Der
Update-Stand lag im Haupt und wurde nur beim Laden des Fensters EINMAL
mitgeschickt. Fand die Pruefung etwas, aenderte sich am Bildschirm
nichts; der einzige Hinweis war ein Satz unter "Update-Adresse", den man
kennen musste, um ihn zu finden.

Was jetzt da ist:

* Eine goldene Karte OBEN in der Uebersicht, sobald etwas gefunden wird
  — mit neuer Versionsnummer, der laufenden Version, Ladefortschritt und
  den Aenderungsnotizen. Dazu ein Windows-Toast beim Fund.
* Knopf "Jetzt auf Updates pruefen" in den Einstellungen. Waehrend eines
  Rips ist er AUS, mit Begruendung im Tooltip — Rippy sucht dann
  bewusst nicht (Paragraph 6.8).
* Knopf "Jetzt neu starten und installieren" auf der Karte, sobald das
  Paket geladen ist. Sagt NEIN mit Grund statt still nichts zu tun (R4).
* Der Stand meldet JEDE Aenderung sofort ans Fenster (setzen() ruft
  immer beiAenderung) — kein Zustand mehr, den niemand erfaehrt.

Der Changelog kommt aus bau/release-notes.md. An der installierten
Bibliothek geprueft statt aus dem Kopf (Regel D): app-builder-lib
out/publish/updateInfoBuilder.js getReleaseInfo() liest genau diesen
Dateinamen und legt den Inhalt als releaseNotes in latest.yml;
builder-util-runtime deklariert UpdateInfo.releaseNotes als
"string | Array<ReleaseNoteInfo> | null". neuerungenText() vertraegt
beide Formen, wirft HTML-Marken raus und deckelt die Laenge.

UpdateStand ist jetzt EINE Definition in gemeinsam/nachrichten.ts, die
sich Haupt und Fenster teilen.

230 Tests gruen (vorher 225), Typpruefung sauber. Version 5.1.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-01 17:51:38 +02:00
co-authored by Claude Opus 5
parent 7d496cf654
commit c0a7618e33
11 changed files with 374 additions and 30 deletions
+81
View File
@@ -782,6 +782,9 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
const s = verbindung.schluessel
const b = verbindung.beschaffung
const empfehlungFuer = (typ: string): string => presetEmpfehlung(typ, backends, presets)
// § 6.8: Der Prüf-Knopf ist während eines Rips aus — nicht, weil er
// nicht könnte, sondern weil Rippy dann bewusst nicht sucht.
const ripLaeuftGerade = Object.values(verbindung.rips).some((r) => LAUFENDE_PHASEN.includes(r.phase))
const [aktiv, setAktiv] = useState('ablage')
const SEKTIONEN: Array<[string, string]> = [
@@ -1032,6 +1035,25 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
{verbindung.kern !== null && ` · Kern: Node ${verbindung.kern.nodeVersion} (PID ${verbindung.kern.pid})`}
</p>
<p className="text-nebel/70">Updates warten, solange ein Rip läuft nie mitten im Vorgang.</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<button
onClick={() => void window.rippy.updatePruefen()}
disabled={ripLaeuftGerade || haupt.update.laeuft}
className="rounded-lg border border-white/15 bg-samt/60 px-3 py-1 text-[11px] text-dunst hover:text-schnee disabled:opacity-35"
title={
ripLaeuftGerade
? 'Während eines Rips wird nicht gesucht — das ist Absicht (§ 6.8).'
: 'Fragt sofort beim Update-Server nach.'
}
>
{haupt.update.laeuft ? 'sucht …' : 'Jetzt auf Updates prüfen'}
</button>
{haupt.update.neueVersion.length > 0 && (
<span className="text-gold-hell">
Version {haupt.update.neueVersion} gefunden siehe Übersicht.
</span>
)}
</div>
</div>
)}
</SektionsKarte>
@@ -1296,6 +1318,9 @@ type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek'
export default function App() {
const [haupt, setHaupt] = useState<HauptStatus | null>(null)
// Sagt ein Update-Knopf NEIN, steht der Grund daneben (R4) — ein Knopf,
// der nichts tut und nichts sagt, ist schlimmer als kein Knopf.
const [updateHinweis, setUpdateHinweis] = useState('')
// #einstellungen / #bibliothek als Startanker — der Smoke-Beweis
// fotografiert damit auch die anderen Bereiche.
const [bereich, setBereich] = useState<Bereich>(() => {
@@ -1407,6 +1432,62 @@ export default function App() {
</div>
)}
{/* § 6.8: Ein verfügbares Update ist eine SICHTBARE Karte, kein
Satz in den Einstellungen. Der Commander musste es am
01.09.2026 suchen — gefunden hat er es nicht. */}
{haupt !== null && haupt.update.neueVersion.length > 0 && (
<div className="rounded-2xl border border-gold/40 bg-gold/10 p-4">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<span className="font-display text-lg text-gold-hell">
Update verfügbar — Version {haupt.update.neueVersion}
</span>
<span className="font-mono text-xs text-nebel">du hast {haupt.version}</span>
</div>
<p className="mt-1 font-mono text-xs text-dunst">{haupt.update.text}</p>
{haupt.update.fortschritt >= 0 && !haupt.update.bereit && (
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-white/10">
<div
className="h-full rounded-full bg-gold transition-all"
style={{ width: `${Math.max(0, Math.min(100, haupt.update.fortschritt))}%` }}
/>
</div>
)}
{haupt.update.neuerungen.length > 0 && (
<div className="mt-3 rounded-xl border border-white/8 bg-buehne/60 p-3">
<p className="text-[11px] uppercase tracking-wider text-nebel">Was neu ist</p>
<p className="mt-1 whitespace-pre-wrap font-mono text-xs leading-5 text-dunst">
{haupt.update.neuerungen}
</p>
</div>
)}
{haupt.update.bereit && (
<div className="mt-3 flex flex-wrap items-center gap-2">
<button
onClick={() => {
setUpdateHinweis('')
void window.rippy.updateInstallieren().then((urteil) => {
if (!urteil.ok) setUpdateHinweis(urteil.grund)
})
}}
disabled={einRipLaeuft}
className="rounded-lg border border-gold/40 bg-gold/15 px-3 py-1.5 text-xs font-semibold text-gold-hell hover:bg-gold/25 disabled:opacity-35"
title={
einRipLaeuft
? 'Erst wenn der Rip fertig ist — Rippy startet dafür neu.'
: 'Rippy beendet sich, installiert und startet wieder.'
}
>
Jetzt neu starten und installieren
</button>
<span className="font-mono text-[11px] text-nebel">
… oder einfach nichts tun: Beim nächsten Beenden passiert es von selbst.
</span>
</div>
)}
{updateHinweis.length > 0 && <p className="mt-2 font-mono text-xs text-warn">{updateHinweis}</p>}
</div>
)}
<section>
{verbindung.laufwerke.length === 0 ? (
<div className="flex items-center gap-5 rounded-2xl border border-white/10 bg-karte/50 p-6">
+2
View File
@@ -9,6 +9,8 @@ declare global {
melden(art: string): void
ordnerWaehlen(titel: string): Promise<string | null>
autostartSetzen(an: boolean): Promise<boolean>
updatePruefen(): Promise<unknown>
updateInstallieren(): Promise<{ ok: boolean; grund: string }>
}
}
}