From bb7089f5604afbae17b835eff3d26766bdcb1a11 Mon Sep 17 00:00:00 2001 From: "Winkler, Stefan" Date: Tue, 1 Sep 2026 11:43:03 +0200 Subject: [PATCH] =?UTF-8?q?Info-Men=C3=BC=20mit=20Benutzerhandbuch/Disclai?= =?UTF-8?q?mer-Dialog=20ergaenzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues (i)-Symbol in der Titelleiste öffnet ein Dropdown mit den Einträgen "Benutzerhandbuch" und "Disclaimer". Klick auf einen Eintrag zeigt den jeweiligen Text als modalen, scrollbaren Dialog (marked wandelt die .md-Dateien zur Laufzeit im Main-Prozess in HTML um). Beide Dateien sind jetzt auch in build.files gelistet, damit sie im gepackten Release verfügbar sind. Claude-Session: https://claude.ai/code/session_01VeqoaAZDVSCU4QBtiYDC5C --- package-lock.json | 15 ++++ package.json | 5 ++ src/info-preload.js | 14 +++ src/info/app.js | 27 ++++++ src/info/index.html | 20 +++++ src/info/styles.css | 188 ++++++++++++++++++++++++++++++++++++++++ src/main/index.js | 85 ++++++++++++++++++ src/preload.js | 4 + src/renderer/app.js | 27 ++++++ src/renderer/index.html | 7 ++ src/renderer/styles.css | 41 +++++++++ 11 files changed, 433 insertions(+) create mode 100644 src/info-preload.js create mode 100644 src/info/app.js create mode 100644 src/info/index.html create mode 100644 src/info/styles.css diff --git a/package-lock.json b/package-lock.json index 47c8295..d30fa51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "claude-live-dashboard", "version": "1.2.1", "license": "MIT", + "dependencies": { + "marked": "^18.0.11" + }, "devDependencies": { "electron": "^43.2.0", "electron-builder": "^25.1.8" @@ -3048,6 +3051,18 @@ "node": ">=12" } }, + "node_modules/marked": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.11.tgz", + "integrity": "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 6f605b8..a655dc8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "src/**/*", "assets/**/*", "config.json", + "BENUTZERHANDBUCH.md", + "DISCLAIMER.md", "!**/*.map" ], "win": { @@ -97,5 +99,8 @@ }, "artifactName": "${productName}-${version}-${arch}.${ext}" } + }, + "dependencies": { + "marked": "^18.0.11" } } diff --git a/src/info-preload.js b/src/info-preload.js new file mode 100644 index 0000000..0574d89 --- /dev/null +++ b/src/info-preload.js @@ -0,0 +1,14 @@ +'use strict'; + +/** Brücke für den Benutzerhandbuch-/Disclaimer-Dialog. Bewusst getrennt vom Widget-Preload. */ + +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('infoBridge', { + /** Registriert den Empfänger für Titel/Inhalt, gesendet bei jedem Öffnen. */ + onContent(callback) { + ipcRenderer.on('info:content', (_event, data) => callback(data)); + }, + /** Schließt das Fenster. */ + close: () => ipcRenderer.send('info:close'), +}); diff --git a/src/info/app.js b/src/info/app.js new file mode 100644 index 0000000..06c8091 --- /dev/null +++ b/src/info/app.js @@ -0,0 +1,27 @@ +'use strict'; + +/** + * Benutzerhandbuch-/Disclaimer-Dialog. Zeigt nur an, was der Main-Prozess + * schickt — Datei lesen und Markdown→HTML wandeln passiert dort (siehe + * `openInfoDoc()` in `src/main/index.js`). + */ + +const $ = (id) => document.getElementById(id); + +const el = { + title: $('title'), + content: $('content'), + close: $('btn-close'), +}; + +window.infoBridge.onContent(({ title, html }) => { + document.title = title; + el.title.textContent = title; + el.content.innerHTML = html; + el.content.scrollTop = 0; +}); + +el.close.addEventListener('click', () => window.infoBridge.close()); +document.addEventListener('keydown', (ev) => { + if (ev.key === 'Escape') window.infoBridge.close(); +}); diff --git a/src/info/index.html b/src/info/index.html new file mode 100644 index 0000000..6a548c9 --- /dev/null +++ b/src/info/index.html @@ -0,0 +1,20 @@ + + + + + + Info + + + +
+ Info + + +
+ +
+ + + + diff --git a/src/info/styles.css b/src/info/styles.css new file mode 100644 index 0000000..3bf2d1d --- /dev/null +++ b/src/info/styles.css @@ -0,0 +1,188 @@ +/* Benutzerhandbuch-/Disclaimer-Fenster — gleiche Farbwelt wie das Widget, + plus Basis-Typografie für aus Markdown gerendertes HTML. */ + +:root { + color-scheme: dark; + + --surface: #1e1e22; + --surface-raised: rgba(255, 255, 255, 0.05); + + --text-primary: #ffffff; + --text-secondary: #c3c2b7; + --text-muted: #898781; + --hairline: rgba(255, 255, 255, 0.1); + + --accent: #3987e5; + + --font: system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: ui-monospace, "Cascadia Mono", Consolas, monospace; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + height: 100%; + background: var(--surface); + color: var(--text-primary); + font-family: var(--font); + font-size: 13px; + overflow: hidden; +} + +body { + display: flex; + flex-direction: column; +} + +/* ── Titelleiste ─────────────────────────────────────────────────────── */ + +.titlebar { + -webkit-app-region: drag; + display: flex; + align-items: center; + gap: 8px; + padding: 9px 10px 9px 14px; + border-bottom: 1px solid var(--hairline); + flex: 0 0 auto; +} + +.title { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); +} + +.spacer { + flex: 1; +} + +.icon-btn { + -webkit-app-region: no-drag; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 22px; + border: 0; + border-radius: 4px; + background: transparent; + color: var(--text-muted); + font-size: 13px; + cursor: pointer; +} + +.icon-btn:hover { + background: var(--surface-raised); + color: var(--text-primary); +} + +/* ── Inhalt (aus Markdown gerendertes HTML) ─────────────────────────────── */ + +.content { + flex: 1 1 auto; + overflow-y: auto; + padding: 16px 20px 22px; + line-height: 1.5; + color: var(--text-secondary); +} + +.content::-webkit-scrollbar { + width: 8px; +} + +.content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.12); + border-radius: 4px; +} + +.content h1, +.content h2, +.content h3 { + color: var(--text-primary); + line-height: 1.3; +} + +.content h1 { + margin: 0 0 12px; + font-size: 18px; + padding-bottom: 8px; + border-bottom: 1px solid var(--hairline); +} + +.content h2 { + margin: 24px 0 8px; + font-size: 14px; +} + +.content h3 { + margin: 18px 0 6px; + font-size: 13px; +} + +.content p { + margin: 0 0 12px; +} + +.content ul, +.content ol { + margin: 0 0 12px; + padding-left: 22px; +} + +.content li { + margin-bottom: 4px; +} + +.content a { + color: var(--accent); +} + +.content code { + font-family: var(--font-mono); + font-size: 12px; + background: var(--surface-raised); + border-radius: 3px; + padding: 1px 4px; +} + +.content pre { + background: var(--surface-raised); + border-radius: 6px; + padding: 10px 12px; + overflow-x: auto; +} + +.content pre code { + background: none; + padding: 0; +} + +.content hr { + border: 0; + border-top: 1px solid var(--hairline); + margin: 20px 0; +} + +.content table { + border-collapse: collapse; + width: 100%; + margin: 0 0 16px; + font-size: 12px; +} + +.content th, +.content td { + border: 1px solid var(--hairline); + padding: 6px 8px; + text-align: left; + vertical-align: top; +} + +.content th { + color: var(--text-primary); + background: var(--surface-raised); +} diff --git a/src/main/index.js b/src/main/index.js index 61f15c5..d62c729 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -8,6 +8,7 @@ const fs = require('node:fs'); const path = require('node:path'); const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeImage, shell, Notification } = require('electron'); +const { marked } = require('marked'); const { Store, loadConfig } = require('./store'); const { Collector } = require('./collector'); @@ -30,6 +31,16 @@ const SETTINGS_HEIGHT = 660; const ERRORS_WIDTH = 420; const ERRORS_HEIGHT = 480; +const INFO_WIDTH = 560; +const INFO_HEIGHT = 680; + +/** Feste Whitelist — der Renderer kann nur diese beiden Schlüssel anfragen, + * nie einen beliebigen Dateipfad. */ +const INFO_DOCS = { + manual: { title: 'Benutzerhandbuch', file: path.join(ROOT, 'BENUTZERHANDBUCH.md') }, + disclaimer: { title: 'Disclaimer', file: path.join(ROOT, 'DISCLAIMER.md') }, +}; + /** Wartezeit vor der einmaligen Update-Prüfung — verzögert, damit sie den * Start nicht verlangsamt und nicht mit dem ersten Poll konkurriert. */ const UPDATE_CHECK_DELAY_MS = 60 * 1000; @@ -37,6 +48,7 @@ const UPDATE_CHECK_DELAY_MS = 60 * 1000; let win = null; let settingsWin = null; let errorsWin = null; +let infoWin = null; let tray = null; let collector = null; let store = null; @@ -364,6 +376,76 @@ function openErrorLog() { }); } +/** Escaped Text für die Einbettung in HTML — nur für die Fehlermeldung unten. */ +function escapeHtml(text) { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>'); +} + +/** + * Benutzerhandbuch-/Disclaimer-Dialog — ein Fenster für beide Dokumente + * (Singleton wie oben), Titel und Inhalt werden bei jedem Öffnen aktualisiert. + * Modal relativ zum Hauptfenster, ausschließlich mit Schließen-Button. + */ +function openInfoDoc(kind) { + const doc = INFO_DOCS[kind]; + if (!doc) return; + + let html; + try { + const raw = fs.readFileSync(doc.file, 'utf8'); + html = marked.parse(raw); + } catch (err) { + html = `

Datei konnte nicht gelesen werden: ${escapeHtml(err.message)}

`; + } + const send = () => infoWin.webContents.send('info:content', { title: doc.title, html }); + + if (infoWin && !infoWin.isDestroyed()) { + infoWin.show(); + infoWin.focus(); + send(); + return; + } + + const target = win && !win.isDestroyed() + ? screen.getDisplayMatching(win.getBounds()) + : screen.getPrimaryDisplay(); + const area = target.workArea; + + infoWin = new BrowserWindow({ + width: INFO_WIDTH, + height: INFO_HEIGHT, + x: Math.round(area.x + (area.width - INFO_WIDTH) / 2), + y: Math.round(area.y + Math.max(0, (area.height - INFO_HEIGHT) / 2)), + minWidth: 420, + minHeight: 360, + frame: false, + resizable: true, + skipTaskbar: false, + modal: true, + parent: win && !win.isDestroyed() ? win : undefined, + alwaysOnTop: true, + show: false, + backgroundColor: '#1e1e22', + icon: path.join(ROOT, 'assets', 'icon.png'), + webPreferences: { + preload: path.join(__dirname, '..', 'info-preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + }, + }); + + infoWin.loadFile(path.join(__dirname, '..', 'info', 'index.html')); + infoWin.webContents.once('did-finish-load', send); + infoWin.once('ready-to-show', () => infoWin.show()); + infoWin.on('closed', () => { + infoWin = null; + }); +} + /** * Übernimmt geänderte Einstellungen ohne Neustart und schreibt sie in die * Datei. Unbekannte Felder und die `_`-Kommentare bleiben erhalten, weil die @@ -419,6 +501,9 @@ function registerIpc() { const info = collector?.state?.update; if (info?.available && info.htmlUrl) shell.openExternal(info.htmlUrl); }); + ipcMain.on('open-manual', () => openInfoDoc('manual')); + ipcMain.on('open-disclaimer', () => openInfoDoc('disclaimer')); + ipcMain.on('info:close', () => infoWin && infoWin.close()); ipcMain.handle('settings:load', () => ({ config, diff --git a/src/preload.js b/src/preload.js index cb4904a..260eba1 100644 --- a/src/preload.js +++ b/src/preload.js @@ -26,4 +26,8 @@ contextBridge.exposeInMainWorld('dashboard', { openErrorLog: () => ipcRenderer.send('open-error-log'), /** Öffnet die Release-Seite eines gefundenen neuen Updates im Browser. */ openReleasePage: () => ipcRenderer.send('open-release-page'), + /** Öffnet den Benutzerhandbuch-Dialog. */ + openManual: () => ipcRenderer.send('open-manual'), + /** Öffnet den Disclaimer-Dialog. */ + openDisclaimer: () => ipcRenderer.send('open-disclaimer'), }); diff --git a/src/renderer/app.js b/src/renderer/app.js index 9ddda2d..99d2738 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -48,6 +48,11 @@ const el = { staleBadge: $('stale-badge'), btnEndpointStatus: $('btn-endpoint-status'), btnUpdateAvailable: $('btn-update-available'), + infoWrap: $('info-wrap'), + btnInfo: $('btn-info'), + infoMenu: $('info-menu'), + infoMenuManual: $('info-menu-manual'), + infoMenuDisclaimer: $('info-menu-disclaimer'), btnHide: $('btn-hide'), btnSettings: $('btn-settings'), btnExpand: $('btn-expand'), @@ -475,6 +480,28 @@ el.btnSettings.addEventListener('click', () => window.dashboard.openSettings()); el.btnEndpointStatus.addEventListener('click', () => window.dashboard.openErrorLog()); el.btnUpdateAvailable.addEventListener('click', () => window.dashboard.openReleasePage()); +el.btnInfo.addEventListener('click', () => { + el.infoMenu.hidden = !el.infoMenu.hidden; +}); +document.addEventListener('click', (ev) => { + if (!el.infoMenu.hidden && !el.infoWrap.contains(ev.target)) { + el.infoMenu.hidden = true; + } +}); +document.addEventListener('keydown', (ev) => { + if (ev.key === 'Escape' && !el.infoMenu.hidden) { + el.infoMenu.hidden = true; + } +}); +el.infoMenuManual.addEventListener('click', () => { + el.infoMenu.hidden = true; + window.dashboard.openManual(); +}); +el.infoMenuDisclaimer.addEventListener('click', () => { + el.infoMenu.hidden = true; + window.dashboard.openDisclaimer(); +}); + window.dashboard.onState(render); // Reihenfolge: erst die Ansicht auf die wiederhergestellte Fenstergröße diff --git a/src/renderer/index.html b/src/renderer/index.html index 3c7bfa1..2c62954 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -30,6 +30,13 @@ /> +
+ + +
diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 085f7f0..40fbdbb 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -157,6 +157,47 @@ body { color: var(--status-update); } +.info-wrap { + position: relative; + display: flex; + align-items: center; +} + +.info-menu { + position: absolute; + top: calc(100% + 4px); + right: 0; + z-index: 10; + display: flex; + flex-direction: column; + min-width: 148px; + padding: 4px; + background: var(--surface-solid); + border: 1px solid var(--hairline); + border-radius: 8px; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); +} + +.info-menu-item { + -webkit-app-region: no-drag; + display: block; + width: 100%; + padding: 6px 8px; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--text-secondary); + font-family: inherit; + font-size: 12px; + text-align: left; + cursor: pointer; +} + +.info-menu-item:hover { + background: var(--surface-raised); + color: var(--text-primary); +} + /* ── Meter ───────────────────────────────────────────────────────────── */ .meters {