feat(v5): Etappe W-6 — Setup, Selbst-Update, Erst-Einrichtung, Lizenzen
Ampel / ampel (push) Successful in 1m22s
Ampel / ampel (push) Successful in 1m22s
electron-builder → NSIS: EIN RippySetup-<version>.exe, pro Benutzer, ohne Adminrechte, eigenes Icon (PNG-basiertes ICO selbst gebaut). extraResources: die MITGELIEFERTEN Werkzeuge (HandBrakeCLI GPL-2, flac.exe MIT libFLAC.dll — die Ohne-DLL-Falle aus § 9) samt Lizenztexten und Quellverweisen (bau/lizenzen, Entscheid 9); vendor/ liegt nicht im Git (74 MB), Befuellung steht in BAUEN.md. Der Kern findet die mitgelieferten Werkzeuge ueber RIPPY_VENDOR (resourcesvendor) VOR allen anderen Suchwegen. haupt/update.ts (§ 6.8): electron-updater gegen das oeffentliche Gitea (festes Tag 'aktuell' als Rueckfallebene, Entscheid 6) — NUR https (http wird ABGELEHNT; Regel-Tests), NIE waehrend eines Rips (der Haupt kennt die aktiven Vorgaenge), Fehlschlaege still aber ablesbar (letzte erfolgreiche Pruefung im Status), installiert beim naechsten Start. haupt/autostart.ts: HKCU-Run ueber app.setLoginItemSettings, Toggle in den Einstellungen. Erst-Einrichtung (§ 6.9) im Programm: 5 Schritte, NUR ein Fehler blockiert (kein Laufwerk = Warnung, MakeMKV-Pflicht mit Klartext-Weg). GEBAUT UND GEMESSEN (30.08.2026): - dist-setup/RippySetup-5.0.0-w0.exe — 130,8 MB, SHA-256 07D39D6BAB74A47C21AA53D847FB5A5DB6990AE9FA1CBC7BE814B51050883115, plus .blockmap. - MESSUNG: Die Update-Auskunft heisst bei Vorab-Versionen nach dem KANAL (w0.yml) — erst ein Release ohne Suffix erzeugt latest.yml (steht jetzt in BAUEN.md). - Das GEPACKTE Programm (win-unpacked, asar + resources) besteht den Smoke: SMOKE OK, und der Screenshot zeigt die Erst-Einrichtung — das Bild eines frischen Rechners. Bewusst offen (SAVEPOINT): die automatische MakeMKV-Beschaffung (beschaffen.ts — Download+Installer-Start braucht einen Live-Test); das Setup NENNT den Weg klar. 185 Tests gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2d9cef22b3
commit
9fa3df500d
@@ -0,0 +1,13 @@
|
||||
// Autostart über HKCU\...\Run (KONZEPT § 8) — via Electrons eigener
|
||||
// API (app.setLoginItemSettings nutzt unter Windows genau diesen
|
||||
// Registry-Zweig). Kein Dienst: Ein Dienst kostete bei jeder Installation
|
||||
// eine Administrator-Abfrage und könnte kein Tray zeigen.
|
||||
import { app } from 'electron'
|
||||
|
||||
export function autostartSetzen(an: boolean): void {
|
||||
app.setLoginItemSettings({ openAtLogin: an })
|
||||
}
|
||||
|
||||
export function autostartAktiv(): boolean {
|
||||
return app.getLoginItemSettings().openAtLogin
|
||||
}
|
||||
@@ -12,10 +12,12 @@ import { app, dialog, ipcMain, Notification, type BrowserWindow } from 'electron
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { HauptStatus, KernNachricht } from '../gemeinsam/nachrichten'
|
||||
import { autostartAktiv, autostartSetzen } from './autostart'
|
||||
import { fensterErstellen } from './fenster'
|
||||
import { KernVerwaltung } from './kernstart'
|
||||
import { leineSetzen, type LeineStatus } from './leine'
|
||||
import { trayErstellen, trayHinweis } from './tray'
|
||||
import { updateEinrichten, updateStand } from './update'
|
||||
|
||||
const smokeModus = process.argv.includes('--smoke')
|
||||
|
||||
@@ -44,6 +46,10 @@ function start(): void {
|
||||
let fenster: BrowserWindow | null = null
|
||||
let kernBereit: Extract<KernNachricht, { art: 'kern-bereit' }> | null = null
|
||||
let beendenGewollt = smokeModus
|
||||
// § 6.8 Regel 2: Der Haupt weiß, ob irgendwo gerippt wird — Updates
|
||||
// warten dann.
|
||||
const aktiveVorgaenge = new Set<string>()
|
||||
let updateGestartet = false
|
||||
|
||||
const smoke = {
|
||||
fenster: false,
|
||||
@@ -67,6 +73,17 @@ function start(): void {
|
||||
smokePruefen()
|
||||
} else if (nachricht.art === 'rip-status') {
|
||||
ripStatusVerarbeiten(nachricht.status)
|
||||
} else if (nachricht.art === 'einstellungen') {
|
||||
// Einmalig das Selbst-Update einrichten — die Adresse kommt aus
|
||||
// der EINEN Einstellungs-Quelle im Kern (§ 6.7/§ 6.8).
|
||||
if (!updateGestartet && !smokeModus) {
|
||||
updateGestartet = true
|
||||
updateEinrichten(
|
||||
nachricht.werte['updateUrl'] ?? '',
|
||||
() => aktiveVorgaenge.size > 0,
|
||||
(text) => toast('Rippy-Update', text),
|
||||
)
|
||||
}
|
||||
} else if (nachricht.art === 'disc-info' && nachricht.info.titel.length > 0 && !smokeModus) {
|
||||
// Windows-Toast beim Erkennen (§ 7) — Klick holt das Fenster hoch.
|
||||
toast(
|
||||
@@ -85,6 +102,11 @@ function start(): void {
|
||||
// § 7: Toasts, Taskleisten-Fortschritt und Tray-Zustand aus dem
|
||||
// Rip-Status — die Information, die man will, ohne das Fenster zu öffnen.
|
||||
function ripStatusVerarbeiten(status: Extract<KernNachricht, { art: 'rip-status' }>['status']): void {
|
||||
if (status.phase === 'rippt' || status.phase === 'komprimiert' || status.phase === 'legt-ab' || status.phase === 'liest-info') {
|
||||
aktiveVorgaenge.add(status.id)
|
||||
} else {
|
||||
aktiveVorgaenge.delete(status.id)
|
||||
}
|
||||
if (fenster !== null && !fenster.isDestroyed()) {
|
||||
if ((status.phase === 'rippt' || status.phase === 'komprimiert') && status.prozent >= 0) {
|
||||
fenster.setProgressBar(status.prozent / 100)
|
||||
@@ -126,6 +148,8 @@ function start(): void {
|
||||
version: app.getVersion(),
|
||||
leine,
|
||||
kern: kernVerwaltung.zustand(),
|
||||
autostart: autostartAktiv(),
|
||||
update: updateStand(),
|
||||
}
|
||||
fenster.webContents.send('haupt-status', status)
|
||||
}
|
||||
@@ -173,6 +197,13 @@ function start(): void {
|
||||
}
|
||||
})
|
||||
|
||||
// Autostart (§ 8): HKCU\...\Run über Electrons eigene API.
|
||||
ipcMain.handle('autostart-setzen', (_ereignis, an: unknown) => {
|
||||
autostartSetzen(an === true)
|
||||
statusSenden()
|
||||
return autostartAktiv()
|
||||
})
|
||||
|
||||
// Echter Windows-Ordnerdialog (§ 7) — der Nutzer sieht seine
|
||||
// Netzlaufwerke und Schnellzugriffe statt eines nachgebauten Browsers.
|
||||
ipcMain.handle('ordner-waehlen', async (_ereignis, titel: unknown) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// utilityProcess statt child_process.fork ist Electrons eigene Empfehlung;
|
||||
// nur er kann einen MessagePort DIREKT zwischen Kern und Fenster aufspannen
|
||||
// (Electron-Doku »utilityProcess« / »MessagePorts in Electron«).
|
||||
import { MessageChannelMain, utilityProcess, type UtilityProcess, type WebContents } from 'electron'
|
||||
import { app, MessageChannelMain, utilityProcess, type UtilityProcess, type WebContents } from 'electron'
|
||||
import { join } from 'node:path'
|
||||
import { istKernNachricht, type HauptNachricht, type KernNachricht } from '../gemeinsam/nachrichten'
|
||||
|
||||
@@ -35,9 +35,16 @@ export class KernVerwaltung {
|
||||
// Der Smoke-Beweis läuft ohne Netz und ohne MakeMKV — der Kern lässt
|
||||
// die Schlüsselkette dann aus.
|
||||
if (this.smokeModus) args.push('--smoke-kern')
|
||||
// Im installierten Rippy liegen die mitgelieferten Werkzeuge unter
|
||||
// resources\vendor — der Kern findet sie über RIPPY_VENDOR (§ 9).
|
||||
const umgebung: NodeJS.ProcessEnv = { ...process.env }
|
||||
if (app.isPackaged) {
|
||||
umgebung['RIPPY_VENDOR'] = join(process.resourcesPath, 'vendor')
|
||||
}
|
||||
const kern = utilityProcess.fork(kernDatei, args, {
|
||||
serviceName: 'Rippy-Kern',
|
||||
stdio: 'inherit',
|
||||
env: umgebung,
|
||||
})
|
||||
this.kern = kern
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Die reinen Update-Regeln (§ 6.8) — getrennt von update.ts, damit die
|
||||
// Tests sie ohne Electron laden können.
|
||||
|
||||
export const STANDARD_UPDATE_URL =
|
||||
'https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/rippy/releases/download/aktuell'
|
||||
|
||||
/** Prüft die Update-Adresse: NUR https:// ist zulässig (§ 6.8 Regel 1) —
|
||||
* ohne Code-Signing (Entscheid 5) ist der Transportweg die einzige
|
||||
* Absicherung; eine http://-Adresse wird ABGELEHNT, nicht still
|
||||
* akzeptiert. */
|
||||
export function updateUrlPruefen(url: string): { ok: boolean; grund: string } {
|
||||
const sauber = url.trim()
|
||||
if (sauber.length === 0) return { ok: true, grund: '' } // Standard gilt
|
||||
if (sauber.startsWith('https://')) return { ok: true, grund: '' }
|
||||
return {
|
||||
ok: false,
|
||||
grund:
|
||||
'Die Update-Adresse muss mit https:// beginnen — ohne Signatur ist der sichere Transportweg die einzige Absicherung (KONZEPT § 6.8).',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Selbst-Update (KONZEPT § 6.8) — electron-updater gegen die Releases im
|
||||
// ö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.
|
||||
// 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).
|
||||
import { app } from 'electron'
|
||||
import electronUpdater from 'electron-updater'
|
||||
import { updateUrlPruefen } from './update-regeln'
|
||||
|
||||
export { STANDARD_UPDATE_URL, updateUrlPruefen } from './update-regeln'
|
||||
|
||||
const { autoUpdater } = electronUpdater
|
||||
|
||||
export interface UpdateStand {
|
||||
/** Wann die letzte Prüfung GELANG — '' wenn noch nie. */
|
||||
letzteErfolgreichePruefung: string
|
||||
text: string
|
||||
}
|
||||
|
||||
const stand: UpdateStand = { letzteErfolgreichePruefung: '', text: 'noch nicht geprüft' }
|
||||
|
||||
export function updateStand(): UpdateStand {
|
||||
return { ...stand }
|
||||
}
|
||||
|
||||
export function updateEinrichten(
|
||||
eigeneUrl: string,
|
||||
ripLaeuft: () => boolean,
|
||||
melden: (text: string) => void,
|
||||
): void {
|
||||
if (!app.isPackaged) {
|
||||
stand.text = 'Entwicklungslauf — Selbst-Update ist nur im installierten Rippy aktiv.'
|
||||
return
|
||||
}
|
||||
const pruefung = updateUrlPruefen(eigeneUrl)
|
||||
if (!pruefung.ok) {
|
||||
stand.text = pruefung.grund
|
||||
melden(pruefung.grund)
|
||||
return
|
||||
}
|
||||
if (eigeneUrl.trim().length > 0) {
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url: eigeneUrl.trim() })
|
||||
}
|
||||
autoUpdater.autoDownload = false
|
||||
// Installiert wird beim NÄCHSTEN Start — nie mitten in einem Rip.
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
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.'
|
||||
return
|
||||
}
|
||||
void autoUpdater.downloadUpdate().catch((fehler: unknown) => {
|
||||
stand.text = `Update-Download fehlgeschlagen: ${String(fehler)}`
|
||||
})
|
||||
})
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
stand.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.'
|
||||
})
|
||||
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)}`
|
||||
})
|
||||
|
||||
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?.()
|
||||
}
|
||||
Reference in New Issue
Block a user