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
+17
View File
@@ -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.
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
+8
View File
@@ -56,4 +56,12 @@ contextBridge.exposeInMainWorld('rippy', {
autostartSetzen(an: boolean): Promise<boolean> {
return ipcRenderer.invoke('autostart-setzen', an === true) as Promise<boolean>
},
/** „Jetzt prüfen" (§ 6.8) — das Ergebnis kommt über den Haupt-Status. */
updatePruefen(): Promise<unknown> {
return ipcRenderer.invoke('update-pruefen') as Promise<unknown>
},
/** „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 }>
},
})
+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 }>
}
}
}
+22 -2
View File
@@ -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<string, unknown> {
+20 -1
View File
@@ -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)
+41
View File
@@ -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<ReleaseNoteInfo> | 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 ''
}
+133 -23
View File
@@ -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<UpdateStand>): 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: '' }
}
+47 -1
View File
@@ -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<ReleaseNoteInfo> | 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('<b>fett</b> und <script>boese()</script> 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)
})
})