From c0a7618e33ea514a11a77fef1f6e892f8457bef3 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Tue, 1 Sep 2026 17:51:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(v5):=20Update=20sichtbar=20machen=20?= =?UTF-8?q?=E2=80=94=20Meldung,=20Knopf=20und=20Changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 | 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 --- rippy-windows/bau/release-notes.md | 17 +++ rippy-windows/package-lock.json | 4 +- rippy-windows/package.json | 2 +- rippy-windows/src/bruecke/index.ts | 8 ++ rippy-windows/src/fenster/App.tsx | 81 +++++++++++ rippy-windows/src/fenster/global.d.ts | 2 + rippy-windows/src/gemeinsam/nachrichten.ts | 24 +++- rippy-windows/src/haupt/index.ts | 21 ++- rippy-windows/src/haupt/update-regeln.ts | 41 ++++++ rippy-windows/src/haupt/update.ts | 156 ++++++++++++++++++--- rippy-windows/test/update.test.ts | 48 ++++++- 11 files changed, 374 insertions(+), 30 deletions(-) create mode 100644 rippy-windows/bau/release-notes.md diff --git a/rippy-windows/bau/release-notes.md b/rippy-windows/bau/release-notes.md new file mode 100644 index 0000000..f49e6ed --- /dev/null +++ b/rippy-windows/bau/release-notes.md @@ -0,0 +1,17 @@ +Version 5.1.2 + +Neu: Rippy sagt Bescheid, wenn ein Update da ist. +Bisher hat es sich still im Hintergrund aktualisiert — jetzt steht eine +Karte oben in der Übersicht, mit Versionsnummer und dieser Liste hier. +In den Einstellungen gibt es dazu den Knopf "Jetzt auf Updates prüfen". +Wer nicht warten will, startet über die Karte sofort neu. + +Aus 5.1.1: Serien-Discs werden richtig erkannt. +Disc 2 von "Spartacus: Gods of the Arena" wurde als der Film "Spartacus" +von 1960 erkannt. Rippy fragt jetzt die Disc selbst: Ihr +Inhaltsverzeichnis nennt die Folgen, und daran erkennt Rippy eine Serie, +statt sie zu raten. Disc-Nummer und Folgenzahl stehen neben dem Titel. +Und wenn Rippy sich unsicher ist, sagt es das jetzt, statt sich sicher +zu irren. + +Unverändert: Updates warten immer, bis kein Rip mehr läuft. diff --git a/rippy-windows/package-lock.json b/rippy-windows/package-lock.json index 13d3f9e..1f483bf 100644 --- a/rippy-windows/package-lock.json +++ b/rippy-windows/package-lock.json @@ -1,12 +1,12 @@ { "name": "rippy", - "version": "5.1.1", + "version": "5.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rippy", - "version": "5.1.1", + "version": "5.1.2", "license": "UNLICENSED", "dependencies": { "electron-updater": "^6.8.9", diff --git a/rippy-windows/package.json b/rippy-windows/package.json index 18c867c..57f5b38 100644 --- a/rippy-windows/package.json +++ b/rippy-windows/package.json @@ -1,7 +1,7 @@ { "name": "rippy", "productName": "Rippy", - "version": "5.1.1", + "version": "5.1.2", "description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)", "main": "out/haupt/index.js", "author": "KrBrZ", diff --git a/rippy-windows/src/bruecke/index.ts b/rippy-windows/src/bruecke/index.ts index 6fe3f25..144947f 100644 --- a/rippy-windows/src/bruecke/index.ts +++ b/rippy-windows/src/bruecke/index.ts @@ -56,4 +56,12 @@ contextBridge.exposeInMainWorld('rippy', { autostartSetzen(an: boolean): Promise { return ipcRenderer.invoke('autostart-setzen', an === true) as Promise }, + /** „Jetzt prüfen" (§ 6.8) — das Ergebnis kommt über den Haupt-Status. */ + updatePruefen(): Promise { + return ipcRenderer.invoke('update-pruefen') as Promise + }, + /** „Jetzt neu starten und installieren" — antwortet mit Grund, wenn nicht. */ + updateInstallieren(): Promise<{ ok: boolean; grund: string }> { + return ipcRenderer.invoke('update-installieren') as Promise<{ ok: boolean; grund: string }> + }, }) diff --git a/rippy-windows/src/fenster/App.tsx b/rippy-windows/src/fenster/App.tsx index f33261c..0837fee 100644 --- a/rippy-windows/src/fenster/App.tsx +++ b/rippy-windows/src/fenster/App.tsx @@ -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})`}

Updates warten, solange ein Rip läuft — nie mitten im Vorgang.

+
+ + {haupt.update.neueVersion.length > 0 && ( + + Version {haupt.update.neueVersion} gefunden — siehe Übersicht. + + )} +
)} @@ -1296,6 +1318,9 @@ type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek' export default function App() { const [haupt, setHaupt] = useState(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(() => { @@ -1407,6 +1432,62 @@ export default function App() { )} + {/* § 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 && ( +
+
+ + Update verfügbar — Version {haupt.update.neueVersion} + + du hast {haupt.version} +
+

{haupt.update.text}

+ {haupt.update.fortschritt >= 0 && !haupt.update.bereit && ( +
+
+
+ )} + {haupt.update.neuerungen.length > 0 && ( +
+

Was neu ist

+

+ {haupt.update.neuerungen} +

+
+ )} + {haupt.update.bereit && ( +
+ + + … oder einfach nichts tun: Beim nächsten Beenden passiert es von selbst. + +
+ )} + {updateHinweis.length > 0 &&

{updateHinweis}

} +
+ )} +
{verbindung.laufwerke.length === 0 ? (
diff --git a/rippy-windows/src/fenster/global.d.ts b/rippy-windows/src/fenster/global.d.ts index 76b74c4..0160c50 100644 --- a/rippy-windows/src/fenster/global.d.ts +++ b/rippy-windows/src/fenster/global.d.ts @@ -9,6 +9,8 @@ declare global { melden(art: string): void ordnerWaehlen(titel: string): Promise autostartSetzen(an: boolean): Promise + updatePruefen(): Promise + updateInstallieren(): Promise<{ ok: boolean; grund: string }> } } } diff --git a/rippy-windows/src/gemeinsam/nachrichten.ts b/rippy-windows/src/gemeinsam/nachrichten.ts index d76ee4b..720e8db 100644 --- a/rippy-windows/src/gemeinsam/nachrichten.ts +++ b/rippy-windows/src/gemeinsam/nachrichten.ts @@ -189,8 +189,28 @@ export interface HauptStatus { leine: { ok: boolean; fehler?: string } kern: { laeuft: boolean; pid?: number; neustarts: number } autostart: boolean - /** Selbst-Update (§ 6.8): letzter Prüf-Erfolg + Klartext-Stand. */ - update: { letzteErfolgreichePruefung: string; text: string } + /** Selbst-Update (§ 6.8). */ + update: UpdateStand +} + +/** Der Stand des Selbst-Updates (§ 6.8) — EINE Definition, die sich Haupt + * und Fenster teilen. Seit 01.09.2026 trägt sie auch die gefundene + * Version, ihre Änderungsnotizen und den Ladefortschritt: Der Kanal + * arbeitete vorher korrekt, aber unsichtbar (Commander-Fund). */ +export interface UpdateStand { + /** Wann die letzte Prüfung GELANG — '' wenn noch nie. */ + letzteErfolgreichePruefung: string + text: string + /** Die gefundene neue Version — '' wenn keine bereitsteht. */ + neueVersion: string + /** Was in ihr neu ist (aus latest.yml) — '' wenn nichts hinterlegt ist. */ + neuerungen: string + /** Geladen und wartet auf den Neustart. */ + bereit: boolean + /** Eine Prüfung oder ein Download läuft gerade. */ + laeuft: boolean + /** Ladefortschritt in Prozent; -1 heißt: kein Download unterwegs. */ + fortschritt: number } function istObjekt(wert: unknown): wert is Record { diff --git a/rippy-windows/src/haupt/index.ts b/rippy-windows/src/haupt/index.ts index e63ce91..3e5717e 100644 --- a/rippy-windows/src/haupt/index.ts +++ b/rippy-windows/src/haupt/index.ts @@ -18,7 +18,7 @@ import { fensterErstellen } from './fenster' import { KernVerwaltung } from './kernstart' import { leineSetzen, type LeineStatus } from './leine' import { trayErstellen, trayHinweis } from './tray' -import { updateEinrichten, updateStand } from './update' +import { updateEinrichten, updateInstallierenJetzt, updatePruefenJetzt, updateStand } from './update' const smokeModus = process.argv.includes('--smoke') @@ -83,6 +83,10 @@ function start(): void { nachricht.werte['updateUrl'] ?? '', () => aktiveVorgaenge.size > 0, (text) => toast('Rippy-Update', text), + // Jede Änderung am Update-Stand geht SOFORT ans Fenster — + // vorher lag sie still im Haupt und war erst beim nächsten + // Laden zu sehen (Commander-Fund 01.09.2026). + () => statusSenden(), ) } } else if (nachricht.art === 'installer-starten') { @@ -219,6 +223,21 @@ function start(): void { } }) + // Der Knopf „Jetzt prüfen" (§ 6.8). Der Stand meldet sich über + // beiAenderung von selbst ans Fenster — hier gibt es nichts zurück. + ipcMain.handle('update-pruefen', () => { + updatePruefenJetzt() + return updateStand() + }) + + // Der Knopf „Jetzt neu starten und installieren". Sagt NEIN mit Grund, + // statt still nichts zu tun (R4). + ipcMain.handle('update-installieren', () => { + const urteil = updateInstallierenJetzt() + if (!urteil.ok) statusSenden() + return urteil + }) + // Autostart (§ 8): HKCU\...\Run über Electrons eigene API. ipcMain.handle('autostart-setzen', (_ereignis, an: unknown) => { autostartSetzen(an === true) diff --git a/rippy-windows/src/haupt/update-regeln.ts b/rippy-windows/src/haupt/update-regeln.ts index e7b62cd..2720188 100644 --- a/rippy-windows/src/haupt/update-regeln.ts +++ b/rippy-windows/src/haupt/update-regeln.ts @@ -18,3 +18,44 @@ export function updateUrlPruefen(url: string): { ok: boolean; grund: string } { 'Die Update-Adresse muss mit https:// beginnen — ohne Signatur ist der sichere Transportweg die einzige Absicherung (KONZEPT § 6.8).', } } + +/** So viel Änderungstext zeigt das Fenster höchstens — der Rest wird + * abgeschnitten, damit eine ausufernde Datei die Karte nicht sprengt. */ +export const NEUERUNGEN_HOECHSTENS = 4000 + +/** Die Änderungsnotizen aus `latest.yml` in reinen Text. + * + * `UpdateInfo.releaseNotes` ist laut builder-util-runtime + * `string | Array | null` — bei uns ein String + * (electron-builder legt den Inhalt von `bau/release-notes.md` hinein), + * bei mehreren übersprungenen Versionen eine Liste. Beides muss hier + * ankommen können, sonst steht im Fenster „[object Object]". + * + * HTML-Marken fliegen raus: Der Text kommt von einem Server, und auch + * wenn React ihn ohnehin nur als Text setzt, wird hier nichts + * weitergereicht, was wie Auszeichnung aussieht. */ +export function neuerungenText(roh: unknown): string { + const saubern = (t: string): string => + t + .replace(/<[^>]*>/g, ' ') + .split('\n') + .map((zeile) => zeile.replace(/[ \t]+$/, '')) + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trim() + + if (typeof roh === 'string') return saubern(roh).slice(0, NEUERUNGEN_HOECHSTENS) + if (Array.isArray(roh)) { + const teile: string[] = [] + for (const eintrag of roh) { + if (eintrag === null || typeof eintrag !== 'object') continue + const e = eintrag as { version?: unknown; note?: unknown } + const version = typeof e.version === 'string' ? e.version : '' + const notiz = typeof e.note === 'string' ? saubern(e.note) : '' + if (notiz.length === 0) continue + teile.push(version.length > 0 ? `${version}\n${notiz}` : notiz) + } + return teile.join('\n\n').slice(0, NEUERUNGEN_HOECHSTENS) + } + return '' +} diff --git a/rippy-windows/src/haupt/update.ts b/rippy-windows/src/haupt/update.ts index 59e4bf3..c8cb657 100644 --- a/rippy-windows/src/haupt/update.ts +++ b/rippy-windows/src/haupt/update.ts @@ -2,42 +2,80 @@ // öffentlichen Gitea (Entscheid 6). Die vier Regeln: // 1. NUR HTTPS — ohne Code-Signing (Entscheid 5) ist der Transportweg // die einzige Absicherung; eine http://-Adresse wird ABGELEHNT. -// 2. NIE während eines Rips — weder laden noch installieren. +// 2. NIE während eines Rips — weder prüfen, laden noch installieren. // 3. Ein Fehlschlag ist sichtbar, aber nicht laut: Der Zeitpunkt der // letzten erfolgreichen Prüfung steht im Status. // 4. Die laufende Version ist ablesbar (Fenster + Protokollkopf). +// +// Ergänzt am 01.09.2026 auf Zuruf des Commanders („irgendwie fehlt der +// Knopf ‚Auf Updates prüfen' und die Meldung beim Start"): Der Kanal +// arbeitete korrekt, war aber UNSICHTBAR — der Stand lag im Haupt und +// wurde nur beim Laden des Fensters einmal mitgeschickt. Jetzt trägt er +// zusätzlich die gefundene Version, ihre Änderungsnotizen und den +// Ladefortschritt, und JEDE Änderung meldet sich sofort ans Fenster. import { app } from 'electron' import electronUpdater from 'electron-updater' -import { updateUrlPruefen } from './update-regeln' +import { neuerungenText, updateUrlPruefen } from './update-regeln' -export { STANDARD_UPDATE_URL, updateUrlPruefen } from './update-regeln' +export { + NEUERUNGEN_HOECHSTENS, + STANDARD_UPDATE_URL, + neuerungenText, + updateUrlPruefen, +} from './update-regeln' const { autoUpdater } = electronUpdater -export interface UpdateStand { - /** Wann die letzte Prüfung GELANG — '' wenn noch nie. */ - letzteErfolgreichePruefung: string - text: string +// EINE Definition für Haupt und Fenster (§ 6.7 sinngemäß: eine Quelle). +export type { UpdateStand } from '../gemeinsam/nachrichten' +import type { UpdateStand } from '../gemeinsam/nachrichten' + +const ANFANG: UpdateStand = { + letzteErfolgreichePruefung: '', + text: 'noch nicht geprüft', + neueVersion: '', + neuerungen: '', + bereit: false, + laeuft: false, + fortschritt: -1, } -const stand: UpdateStand = { letzteErfolgreichePruefung: '', text: 'noch nicht geprüft' } +let stand: UpdateStand = { ...ANFANG } +let aenderungMelden: () => void = () => { + // Vor updateEinrichten() gibt es noch kein Fenster, das zuhören könnte. +} +let ripLaeuftJetzt: () => boolean = () => false +let scharf = false export function updateStand(): UpdateStand { return { ...stand } } +/** Stand ändern UND das Fenster benachrichtigen — nie das eine ohne das + * andere: Ein Stand, den niemand erfährt, ist der Zustand von vorher + * (der 26.07.-Fund „ein still scheiternder Hintergrund-Prozess"). */ +function setzen(teil: Partial): void { + stand = { ...stand, ...teil } + aenderungMelden() +} + export function updateEinrichten( eigeneUrl: string, ripLaeuft: () => boolean, melden: (text: string) => void, + beiAenderung: () => void = () => { + // ohne Fenster kein Empfänger — der Stand bleibt trotzdem abrufbar + }, ): void { + aenderungMelden = beiAenderung + ripLaeuftJetzt = ripLaeuft if (!app.isPackaged) { - stand.text = 'Entwicklungslauf — Selbst-Update ist nur im installierten Rippy aktiv.' + setzen({ text: 'Entwicklungslauf — Selbst-Update ist nur im installierten Rippy aktiv.' }) return } const pruefung = updateUrlPruefen(eigeneUrl) if (!pruefung.ok) { - stand.text = pruefung.grund + setzen({ text: pruefung.grund }) melden(pruefung.grund) return } @@ -47,37 +85,109 @@ export function updateEinrichten( autoUpdater.autoDownload = false // Installiert wird beim NÄCHSTEN Start — nie mitten in einem Rip. autoUpdater.autoInstallOnAppQuit = true + scharf = true - autoUpdater.on('update-available', () => { + autoUpdater.on('update-available', (info) => { + const neuerungen = neuerungenText(info.releaseNotes) if (ripLaeuft()) { // Regel 2: nicht einmal herunterladen, solange gerippt wird — der // tägliche Takt versucht es wieder. - stand.text = 'Update verfügbar — wird geladen, sobald kein Rip läuft.' + setzen({ + neueVersion: info.version, + neuerungen, + laeuft: false, + fortschritt: -1, + text: `Version ${info.version} ist da — sie wird geladen, sobald kein Rip mehr läuft.`, + }) + melden(stand.text) return } + setzen({ + neueVersion: info.version, + neuerungen, + laeuft: true, + fortschritt: 0, + text: `Version ${info.version} wird geladen …`, + }) + // Der Commander wollte die Meldung sehen, nicht suchen müssen. + melden(`Update verfügbar: Version ${info.version} wird geladen.`) void autoUpdater.downloadUpdate().catch((fehler: unknown) => { - stand.text = `Update-Download fehlgeschlagen: ${String(fehler)}` + setzen({ + laeuft: false, + fortschritt: -1, + text: `Update-Download fehlgeschlagen: ${String(fehler).slice(0, 160)}`, + }) }) }) + autoUpdater.on('download-progress', (fortschritt) => { + setzen({ laeuft: true, fortschritt: Math.round(fortschritt.percent) }) + }) autoUpdater.on('update-downloaded', (info) => { - stand.text = `Version ${info.version} liegt bereit — sie wird beim nächsten Start installiert.` + setzen({ + neueVersion: info.version, + neuerungen: neuerungenText(info.releaseNotes) || stand.neuerungen, + bereit: true, + laeuft: false, + fortschritt: 100, + text: `Version ${info.version} liegt bereit — sie wird beim nächsten Start installiert.`, + }) melden(stand.text) }) autoUpdater.on('update-not-available', () => { - stand.letzteErfolgreichePruefung = new Date().toISOString() - stand.text = 'Rippy ist aktuell.' + setzen({ + letzteErfolgreichePruefung: new Date().toISOString(), + neueVersion: '', + neuerungen: '', + bereit: false, + laeuft: false, + fortschritt: -1, + text: 'Rippy ist aktuell.', + }) }) autoUpdater.on('error', (fehler) => { // Regel 3: still scheitern (WLAN weg, Server aus) — aber ablesbar. - stand.text = `Letzte Prüfung scheiterte: ${String(fehler).slice(0, 160)}` + setzen({ laeuft: false, fortschritt: -1, text: `Letzte Prüfung scheiterte: ${String(fehler).slice(0, 160)}` }) }) - const pruefen = (): void => { - if (ripLaeuft()) return - void autoUpdater.checkForUpdates().catch(() => { - // der error-Handler oben trägt den Grund ein - }) - } pruefen() setInterval(pruefen, 24 * 60 * 60 * 1000).unref?.() } + +function pruefen(): void { + if (ripLaeuftJetzt()) return + setzen({ laeuft: true, text: 'Suche nach Updates …' }) + void autoUpdater.checkForUpdates().catch(() => { + // der error-Handler oben trägt den Grund ein + }) +} + +/** Der Knopf „Jetzt prüfen" im Fenster. Antwortet IMMER etwas — ein + * Knopf, der nichts sagt, ist schlimmer als keiner. */ +export function updatePruefenJetzt(): void { + if (!scharf) { + // Im Entwicklungslauf steht der Grund schon im Text; ihn erneut zu + // melden hält den Knopf ehrlich, statt ihn ins Leere laufen zu lassen. + aenderungMelden() + return + } + if (ripLaeuftJetzt()) { + setzen({ text: 'Während eines Rips wird nicht nach Updates gesucht — das ist Absicht (§ 6.8).' }) + return + } + pruefen() +} + +/** Der Knopf „Jetzt neu starten und installieren". Nur sinnvoll, wenn + * wirklich etwas bereitliegt — und nie mitten in einem Rip. */ +export function updateInstallierenJetzt(): { ok: boolean; grund: string } { + if (!scharf || !stand.bereit) { + return { ok: false, grund: 'Es liegt kein geladenes Update bereit.' } + } + if (ripLaeuftJetzt()) { + return { ok: false, grund: 'Erst wenn der Rip fertig ist — Rippy startet dafür neu (§ 6.8).' } + } + // quitAndInstall schließt alle Fenster und beendet die App; der + // Installer läuft danach und startet Rippy wieder. + autoUpdater.quitAndInstall() + return { ok: true, grund: '' } +} diff --git a/rippy-windows/test/update.test.ts b/rippy-windows/test/update.test.ts index 63aa94f..c279162 100644 --- a/rippy-windows/test/update.test.ts +++ b/rippy-windows/test/update.test.ts @@ -2,7 +2,12 @@ // Ohne Code-Signing (Entscheid 5) ist der Transportweg die EINZIGE // Absicherung des Selbst-Updates. import { describe, expect, it } from 'vitest' -import { updateUrlPruefen, STANDARD_UPDATE_URL } from '../src/haupt/update-regeln' +import { + neuerungenText, + updateUrlPruefen, + NEUERUNGEN_HOECHSTENS, + STANDARD_UPDATE_URL, +} from '../src/haupt/update-regeln' describe('updateUrlPruefen', () => { it('leer heißt Standard (der ist https)', () => { @@ -22,3 +27,44 @@ describe('updateUrlPruefen', () => { expect(updateUrlPruefen('file://x').ok).toBe(false) }) }) + +// Die Änderungsnotizen kommen aus latest.yml und werden im Fenster +// angezeigt (Commander-Wunsch 01.09.2026: „inkl. changelog"). +// builder-util-runtime deklariert sie als +// `string | Array | null` — beide Formen müssen hier +// ankommen, sonst steht in der Karte „[object Object]". +describe('neuerungenText', () => { + it('nimmt den Text aus bau/release-notes.md, wie er kommt', () => { + expect(neuerungenText('Version 5.1.2\n\nNeu: ein Knopf.\n')).toBe('Version 5.1.2\n\nNeu: ein Knopf.') + }) + + it('nimmt auch die Listenform (mehrere übersprungene Versionen)', () => { + const roh = [ + { version: '5.1.1', note: 'Serien-Discs richtig erkannt.' }, + { version: '5.1.2', note: 'Update-Meldung im Fenster.' }, + ] + const text = neuerungenText(roh) + expect(text).toContain('5.1.1') + expect(text).toContain('Serien-Discs richtig erkannt.') + expect(text).toContain('Update-Meldung im Fenster.') + }) + + it('HTML-Marken fliegen raus — der Text kommt von einem Server', () => { + const text = neuerungenText('fett und Rest') + expect(text).not.toContain('<') + expect(text).not.toContain('script>') + expect(text).toContain('fett') + }) + + it('nichts, Unsinn und Leeres ergeben einen leeren Text — nie [object Object]', () => { + expect(neuerungenText(null)).toBe('') + expect(neuerungenText(undefined)).toBe('') + expect(neuerungenText(42)).toBe('') + expect(neuerungenText([{ version: '1' }])).toBe('') + expect(neuerungenText(' \n ')).toBe('') + }) + + it('eine ausufernde Datei sprengt die Karte nicht', () => { + expect(neuerungenText('x'.repeat(NEUERUNGEN_HOECHSTENS * 2)).length).toBe(NEUERUNGEN_HOECHSTENS) + }) +})