feat(v5): Update sichtbar machen — Meldung, Knopf und Changelog
Ampel / ampel (push) Successful in 1m33s
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:
co-authored by
Claude Opus 5
parent
7d496cf654
commit
c0a7618e33
@@ -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.
|
||||
Generated
+2
-2
@@ -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,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",
|
||||
|
||||
@@ -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 }>
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
@@ -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 }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ''
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
void autoUpdater.downloadUpdate().catch((fehler: unknown) => {
|
||||
stand.text = `Update-Download fehlgeschlagen: ${String(fehler)}`
|
||||
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) => {
|
||||
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: '' }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user