6 Commits
Author SHA1 Message Date
swinksandClaude Opus 4.8 cd29e00707 electron-builder auf 26 heben, npm-Audit-Warnungen beheben
Alle 16 npm-Audit-Findings (15 high, 1 critical) lagen ausschliesslich im
Build-Werkzeug electron-builder/electron (devDependency) und wurden nie ins
Paket ausgeliefert -- die einzige Runtime-Dependency ist marked. `npm audit
fix` (nicht-breakend) plus Anheben von electron-builder 25 -> 26.15.3
bringt den Audit auf 0 Findings, inkl. der AppImage-Search-Path-Luecke
(GHSA-7g7r-gx96-252g), die das gebaute AppImage tatsaechlich betraf.

electron-builder 26 aendert das linux.desktop-Schema: [Desktop Entry]-
Eintraege liegen jetzt unter desktop.entry statt direkt. StartupWMClass
entsprechend verschoben; das gebaute .desktop enthaelt es unveraendert,
die Always-on-Top-Fensterzuordnung bleibt also erhalten. Linux-Build
(AppImage + .deb, v1.2.1) verifiziert: exit 0, .deb meldet 1.2.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXVYWBuRykeGM4mWw3xMe9
2026-09-01 21:36:38 +02:00
Winkler, Stefan 378e2f86e5 Englische Fassung des Disclaimers ergaenzen
Claude-Session: https://claude.ai/code/session_01VeqoaAZDVSCU4QBtiYDC5C
2026-09-01 12:26:32 +02:00
Winkler, Stefan fff2f288cb Info-Menü im Tray ergaenzen, Sprunganker im Benutzerhandbuch beheben
Tray-Menü bekommt einen "Info"-Untermenüpunkt (Benutzerhandbuch/
Disclaimer/Lizenz), analog zum Menü in der Titelleiste. Markdown-
Überschriften erhalten jetzt stabile id-Attribute (marked vergibt seit
Version 5 keine eigenen mehr), damit das neue Inhaltsverzeichnis im
Benutzerhandbuch als Sprunganker funktioniert. Reine Sprunganker im
Info-Fenster navigieren nicht mehr wie externe Links. Nach jedem
Abschnitt verlinkt "[nach oben]" zurück zum Inhaltsverzeichnis.

Claude-Session: https://claude.ai/code/session_01VeqoaAZDVSCU4QBtiYDC5C
2026-09-01 12:17:02 +02:00
Winkler, Stefan f8a286f219 Kaputte Links im Info-Dialog beheben, Lizenz-Menüpunkt ergaenzen
Klicks auf Links im gerenderten Markdown (z. B. BENUTZERHANDBUCH.md,
LICENSE.txt) navigierten das Fenster selbst dorthin und liessen einen
leeren, unschliessbaren Dialog zurueck. Main-Prozess faengt jetzt jede
Navigation im Info-Fenster ab: externe http(s)-Links oeffnen im
Standardbrowser, interne Verweise auf Benutzerhandbuch/Disclaimer/
Lizenz wechseln stattdessen den Inhalt desselben Dialogs.

Neuer dritter Menüpunkt "Lizenz" (LICENSE.txt) im Info-Dropdown. Der
README.md-Link in DISCLAIMER.md zeigt jetzt auf die Repo-Seite auf
Gitea statt auf einen kaputten relativen Pfad.

Claude-Session: https://claude.ai/code/session_01VeqoaAZDVSCU4QBtiYDC5C
2026-09-01 11:58:58 +02:00
Winkler, Stefan bb7089f560 Info-Menü mit Benutzerhandbuch/Disclaimer-Dialog ergaenzen
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
2026-09-01 11:43:03 +02:00
Winkler, Stefan dd560a36cb Haftungsausschluss zu KI-gestützter Entwicklung ergaenzen
Claude-Session: https://claude.ai/code/session_01VeqoaAZDVSCU4QBtiYDC5C
2026-09-01 10:21:49 +02:00
14 changed files with 1759 additions and 2001 deletions
+39 -2
View File
@@ -4,6 +4,22 @@ Dieses Handbuch richtet sich an Anwender, die Claude Live Dashboard installieren
und nutzen möchten. Wer die Anwendung selbst bauen oder am Quellcode arbeiten und nutzen möchten. Wer die Anwendung selbst bauen oder am Quellcode arbeiten
möchte, findet die technische Dokumentation in der `README.md`. möchte, findet die technische Dokumentation in der `README.md`.
## Inhalt
- [Was die Anwendung tut](#was-die-anwendung-tut)
- [Voraussetzungen](#voraussetzungen)
- [Installation](#installation)
- [Windows](#windows)
- [Linux (Ubuntu und verwandte Distributionen)](#linux-ubuntu-und-verwandte-distributionen)
- [Erster Start](#erster-start)
- [Die Oberfläche](#die-oberfläche)
- [Endpunkt-Status und Fehler-/Ereignis-Log](#endpunkt-status-und-fehler-ereignis-log)
- [Einstellungen](#einstellungen)
- [Tray-Menü](#tray-menü)
- [Datenschutz und Sicherheit](#datenschutz-und-sicherheit)
- [Deinstallation](#deinstallation)
- [Häufige Fragen](#häufige-fragen)
## Was die Anwendung tut ## Was die Anwendung tut
Claude Live Dashboard ist ein kleines, immer sichtbares Fenster, das anzeigt, Claude Live Dashboard ist ein kleines, immer sichtbares Fenster, das anzeigt,
@@ -18,12 +34,16 @@ zusätzlich denselben Server-Endpunkt ab, den auch der Befehl `/usage` in
Claude Code nutzt, um exakte statt geschätzter Werte anzuzeigen — dazu weiter Claude Code nutzt, um exakte statt geschätzter Werte anzuzeigen — dazu weiter
unten mehr unter „Datenschutz und Sicherheit“. unten mehr unter „Datenschutz und Sicherheit“.
[nach oben](#inhalt)
## Voraussetzungen ## Voraussetzungen
- Windows 10/11 oder Ubuntu (bzw. eine vergleichbare Linux-Distribution) - Windows 10/11 oder Ubuntu (bzw. eine vergleichbare Linux-Distribution)
- Claude Code muss installiert sein und mindestens einmal benutzt worden sein - Claude Code muss installiert sein und mindestens einmal benutzt worden sein
— ohne vorhandene Protokolldateien hat das Dashboard nichts anzuzeigen — ohne vorhandene Protokolldateien hat das Dashboard nichts anzuzeigen
[nach oben](#inhalt)
## Installation ## Installation
### Windows ### Windows
@@ -56,6 +76,8 @@ Unter GNOME braucht das Tray-Symbol die Erweiterung **AppIndicator** — bei
Standard-Ubuntu ist sie bereits vorinstalliert. Fehlt sie, läuft die Anwendung Standard-Ubuntu ist sie bereits vorinstalliert. Fehlt sie, läuft die Anwendung
trotzdem, nur ohne sichtbares Symbol in der oberen Leiste. trotzdem, nur ohne sichtbares Symbol in der oberen Leiste.
[nach oben](#inhalt)
## Erster Start ## Erster Start
Nach dem Start erscheint das Fenster rechts oben auf dem Bildschirm und bleibt Nach dem Start erscheint das Fenster rechts oben auf dem Bildschirm und bleibt
@@ -66,6 +88,8 @@ Unter Windows liegt das Symbol zunächst häufig im Überlaufbereich hinter dem
Pfeil `^` — per Drag & Drop lässt es sich dauerhaft sichtbar an die Taskleiste Pfeil `^` — per Drag & Drop lässt es sich dauerhaft sichtbar an die Taskleiste
heften. heften.
[nach oben](#inhalt)
## Die Oberfläche ## Die Oberfläche
**Kopfzeile** — per Ziehen lässt sich das Fenster frei positionieren, die **Kopfzeile** — per Ziehen lässt sich das Fenster frei positionieren, die
@@ -76,6 +100,7 @@ Position wird gemerkt.
| Statuspunkt | Zeigt den Zustand der Datenquellen, siehe unten | | Statuspunkt | Zeigt den Zustand der Datenquellen, siehe unten |
| Pfeil-Symbol (nur bei neuem Release) | Öffnet die Release-Seite im Browser | | Pfeil-Symbol (nur bei neuem Release) | Öffnet die Release-Seite im Browser |
| Zahnrad | Öffnet die Einstellungen | | Zahnrad | Öffnet die Einstellungen |
| `ⓘ` | Öffnet ein Menü mit Benutzerhandbuch, Disclaimer und Lizenz — Klick auf einen Eintrag zeigt den Text in einem eigenen Fenster |
| `` | Blendet das Fenster aus (Rückkehr über das Tray-Symbol) | | `` | Blendet das Fenster aus (Rückkehr über das Tray-Symbol) |
**Anzeigebereich** **Anzeigebereich**
@@ -109,6 +134,8 @@ einen Neustart hinweg erhalten und lässt sich über den Knopf **Log löschen**
jederzeit zurücksetzen. Bei einer Fehlermeldung im Programm ist ein Blick in jederzeit zurücksetzen. Bei einer Fehlermeldung im Programm ist ein Blick in
dieses Log meist der schnellste Weg herauszufinden, was los ist. dieses Log meist der schnellste Weg herauszufinden, was los ist.
[nach oben](#inhalt)
## Einstellungen ## Einstellungen
Das Zahnrad öffnet den Einstellungsdialog: Das Zahnrad öffnet den Einstellungsdialog:
@@ -136,12 +163,16 @@ Das Zahnrad öffnet den Einstellungsdialog:
Updates suchen", siehe oben). `Strg`+`S` speichert, `Esc` schließt den Updates suchen", siehe oben). `Strg`+`S` speichert, `Esc` schließt den
Dialog ohne zu speichern. Dialog ohne zu speichern.
[nach oben](#inhalt)
## Tray-Menü ## Tray-Menü
Ein Rechtsklick (Linux) bzw. Klick (Windows) auf das Symbol im Ein Rechtsklick (Linux) bzw. Klick (Windows) auf das Symbol im
Infobereich/der Systemleiste öffnet ein Menü mit: Dashboard anzeigen/ausblenden, Infobereich/der Systemleiste öffnet ein Menü mit: Dashboard anzeigen/ausblenden,
Deckkraft, Klick-Durchlässigkeit, Autostart, Einstellungen, Datenordner öffnen Deckkraft, Klick-Durchlässigkeit, Autostart, Einstellungen, Info
und Beenden. (Benutzerhandbuch/Disclaimer/Lizenz), Datenordner öffnen und Beenden.
[nach oben](#inhalt)
## Datenschutz und Sicherheit ## Datenschutz und Sicherheit
@@ -168,6 +199,8 @@ und Beenden.
Zugriffstoken und ohne weitere Daten. Abschaltbar in den Einstellungen. Zugriffstoken und ohne weitere Daten. Abschaltbar in den Einstellungen.
- Es findet keine Übertragung an Dritte statt. - Es findet keine Übertragung an Dritte statt.
[nach oben](#inhalt)
## Deinstallation ## Deinstallation
**Windows:** über *Einstellungen ▸ Apps* bzw. den mitinstallierten **Windows:** über *Einstellungen ▸ Apps* bzw. den mitinstallierten
@@ -178,6 +211,8 @@ erneut installieren.
**Linux:** `.deb`-Installation mit `sudo apt remove claude-live-dashboard`; **Linux:** `.deb`-Installation mit `sudo apt remove claude-live-dashboard`;
ein AppImage genügt zu löschen. ein AppImage genügt zu löschen.
[nach oben](#inhalt)
## Häufige Fragen ## Häufige Fragen
**Das Fenster zeigt keine Zahlen an.** Vermutlich wurde Claude Code auf diesem **Das Fenster zeigt keine Zahlen an.** Vermutlich wurde Claude Code auf diesem
@@ -209,3 +244,5 @@ oder Symbol öffnet die Release-Seite im Browser — heruntergeladen oder
installiert wird nichts automatisch. Das Symbol bleibt sichtbar, bis eine installiert wird nichts automatisch. Das Symbol bleibt sichtbar, bis eine
neuere Version installiert wurde. Abschaltbar über die Einstellung „Nach neuere Version installiert wurde. Abschaltbar über die Einstellung „Nach
Updates suchen". Updates suchen".
[nach oben](#inhalt)
+103
View File
@@ -0,0 +1,103 @@
# Haftungsausschluss
## KI-gestützte Entwicklung
Diese Anwendung wurde überwiegend unter Zuhilfenahme von KI-Werkzeugen
(Claude Code, Anthropic) entwickelt — von der Implementierung über
Refactorings bis zu Teilen der Dokumentation. Ein menschlicher Entwickler hat
Anforderungen vorgegeben, Änderungen geprüft und freigegeben; dennoch können
KI-generierte Anteile Fehler, Unklarheiten oder unübliche Muster enthalten,
die bei rein manuell entwickelter Software seltener vorkämen. Fehler bitte
als Issue im Repository melden.
## Keine offizielle Verbindung zu Anthropic
Dieses Projekt ist ein privates, inoffizielles Werkzeug und steht in keiner
Verbindung zu Anthropic. Es wird nicht von Anthropic bereitgestellt,
unterstützt oder geprüft. „Claude“ und verwandte Bezeichnungen sind Marken
ihrer jeweiligen Inhaber und werden hier nur zur Beschreibung der
Kompatibilität verwendet.
## Undokumentierte Schnittstelle
Die optionale Serverabfrage (Einstellung „Live-Kalibrierung“/`oauth`) nutzt
einen Endpunkt, den Anthropic nicht öffentlich dokumentiert und nicht als
stabil zusichert (siehe `src/main/oauth-usage.js`). Er kann sich jederzeit
ändern oder ohne Vorankündigung wegfallen. Fällt er aus, rechnet die
Anwendung automatisch mit lokal kalibrierten Näherungswerten weiter — das ist
kein Fehlerzustand, sondern die vorgesehene Rückfallebene.
## Keine Gewährleistung für angezeigte Werte
Die angezeigten Auslastungswerte, Limits und Prognosen sind bestmögliche
Schätzungen bzw. — wo verfügbar — vom Server gemeldete Werte. Sie ersetzen
nicht die offizielle Nutzungsanzeige in Claude Code (`/usage`) und sind keine
Grundlage für Abrechnungs- oder Vertragsentscheidungen. Für die korrekte
Abrechnung ist ausschließlich Anthropic maßgeblich.
## Datenverarbeitung
Die Anwendung liest lokal vorhandene Claude-Code-Transkripte und optional das
lokale Zugriffstoken, um die genannte Serverabfrage durchzuführen. Details
dazu stehen im Abschnitt „Datenschutz und Sicherheit“ in
[`BENUTZERHANDBUCH.md`](BENUTZERHANDBUCH.md) sowie in
[`README.md`](https://git.conatum.net/swinks/claude-live-dashboard/src/branch/main/README.md).
## Lizenz und Nutzung auf eigene Verantwortung
Die Software wird, wie in [`LICENSE.txt`](LICENSE.txt) (MIT-Lizenz)
festgehalten, ohne jegliche Gewährleistung bereitgestellt. Nutzung und
Installation erfolgen auf eigene Verantwortung.
---
# Disclaimer (English)
## AI-Assisted Development
This application was largely developed with the help of AI tools (Claude
Code, Anthropic) — from implementation through refactoring to parts of the
documentation. A human developer specified requirements and reviewed and
approved every change; nonetheless, AI-generated portions may contain bugs,
ambiguities, or unusual patterns less common in purely manually developed
software. Please report issues in the repository.
## No Official Affiliation with Anthropic
This project is a private, unofficial tool and has no affiliation with
Anthropic. It is not provided, endorsed, or reviewed by Anthropic. "Claude"
and related names are trademarks of their respective owners and are used
here solely to describe compatibility.
## Undocumented Interface
The optional server query (setting "live calibration"/`oauth`) uses an
endpoint that Anthropic does not publicly document and does not guarantee
as stable (see `src/main/oauth-usage.js`). It may change or disappear
without notice at any time. If it becomes unavailable, the application
automatically continues with locally calibrated approximations — this is
not an error state but the intended fallback.
## No Warranty for Displayed Values
The usage figures, limits, and projections shown are best-effort estimates
or, where available, values reported by the server. They do not replace
the official usage display in Claude Code (`/usage`) and are not a basis
for billing or contractual decisions. Anthropic alone is authoritative for
correct billing.
## Data Processing
The application reads locally stored Claude Code transcripts and,
optionally, the local access token to perform the server query described
above. Details are covered in the "Datenschutz und Sicherheit" (data
protection and security) section of the German user manual
([`BENUTZERHANDBUCH.md`](BENUTZERHANDBUCH.md)) and in
[`README.md`](https://git.conatum.net/swinks/claude-live-dashboard/src/branch/main/README.md)
(German).
## License and Use at Your Own Risk
The software is provided without any warranty, as stated in
[`LICENSE.txt`](LICENSE.txt) (MIT License). Use and installation are at
your own risk.
+1
View File
@@ -75,6 +75,7 @@ ELECTRON_OZONE_PLATFORM_HINT=wayland ./start.sh # nativ Wayland, ohne Position
| Kopfzeile ziehen | Fenster verschieben (Position wird gemerkt) | | Kopfzeile ziehen | Fenster verschieben (Position wird gemerkt) |
| Statuspunkt (links vom Zahnrad) | Fehler-/Ereignis-Log öffnen | | Statuspunkt (links vom Zahnrad) | Fehler-/Ereignis-Log öffnen |
| ⚙ | Einstellungen öffnen | | ⚙ | Einstellungen öffnen |
| ⓘ | Menü mit Benutzerhandbuch/Disclaimer/Lizenz — öffnet den jeweiligen Text in einem eigenen Fenster |
| „Details“ | Modell-Aufschlüsselung, Tempo und Datengrundlage aufklappen | | „Details“ | Modell-Aufschlüsselung, Tempo und Datengrundlage aufklappen |
| `` | Ausblenden — Rückkehr über das Tray-Symbol | | `` | Ausblenden — Rückkehr über das Tray-Symbol |
| Tray-Klick | Ein-/Ausblenden | | Tray-Klick | Ein-/Ausblenden |
+1094 -2003
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -21,7 +21,7 @@
"homepage": "https://github.com/exilkieler/claude-live-dashboard", "homepage": "https://github.com/exilkieler/claude-live-dashboard",
"devDependencies": { "devDependencies": {
"electron": "^43.2.0", "electron": "^43.2.0",
"electron-builder": "^25.1.8" "electron-builder": "^26.15.3"
}, },
"build": { "build": {
"appId": "de.local.claude-live-dashboard", "appId": "de.local.claude-live-dashboard",
@@ -35,6 +35,9 @@
"src/**/*", "src/**/*",
"assets/**/*", "assets/**/*",
"config.json", "config.json",
"BENUTZERHANDBUCH.md",
"DISCLAIMER.md",
"LICENSE.txt",
"!**/*.map" "!**/*.map"
], ],
"win": { "win": {
@@ -93,9 +96,14 @@
"synopsis": "Auslastung des Claude-Abos im Blick", "synopsis": "Auslastung des Claude-Abos im Blick",
"description": "Rahmenloses Always-on-Top-Widget, das die Auslastung des Claude-Abos aus den lokalen Claude-Code-Transkripten liest.", "description": "Rahmenloses Always-on-Top-Widget, das die Auslastung des Claude-Abos aus den lokalen Claude-Code-Transkripten liest.",
"desktop": { "desktop": {
"entry": {
"StartupWMClass": "claude-live-dashboard" "StartupWMClass": "claude-live-dashboard"
}
}, },
"artifactName": "${productName}-${version}-${arch}.${ext}" "artifactName": "${productName}-${version}-${arch}.${ext}"
} }
},
"dependencies": {
"marked": "^18.0.11"
} }
} }
+14
View File
@@ -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'),
});
+27
View File
@@ -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();
});
+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; script-src 'self';" />
<title>Info</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header class="titlebar">
<span class="title" id="title">Info</span>
<span class="spacer"></span>
<button class="icon-btn" id="btn-close" title="Schließen" aria-label="Schließen"></button>
</header>
<main class="content" id="content"></main>
<script src="app.js"></script>
</body>
</html>
+188
View File
@@ -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);
}
+182
View File
@@ -8,6 +8,7 @@
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeImage, shell, Notification } = require('electron'); const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeImage, shell, Notification } = require('electron');
const { marked } = require('marked');
const { Store, loadConfig } = require('./store'); const { Store, loadConfig } = require('./store');
const { Collector } = require('./collector'); const { Collector } = require('./collector');
@@ -30,6 +31,28 @@ const SETTINGS_HEIGHT = 660;
const ERRORS_WIDTH = 420; const ERRORS_WIDTH = 420;
const ERRORS_HEIGHT = 480; const ERRORS_HEIGHT = 480;
const INFO_WIDTH = 560;
const INFO_HEIGHT = 680;
/** Feste Whitelist — der Renderer kann nur diese 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') },
license: { title: 'Lizenz', file: path.join(ROOT, 'LICENSE.txt') },
};
/**
* Ordnet Dateinamen, wie sie in den Markdown-Texten oben verlinkt sind, dem
* passenden INFO_DOCS-Schlüssel zu — für Links, die innerhalb der Anwendung
* bleiben sollen (siehe will-navigate-Abfangen in openInfoDoc()).
*/
const INFO_LINK_TARGETS = {
'BENUTZERHANDBUCH.md': 'manual',
'DISCLAIMER.md': 'disclaimer',
'LICENSE.txt': 'license',
};
/** Wartezeit vor der einmaligen Update-Prüfung — verzögert, damit sie den /** Wartezeit vor der einmaligen Update-Prüfung — verzögert, damit sie den
* Start nicht verlangsamt und nicht mit dem ersten Poll konkurriert. */ * Start nicht verlangsamt und nicht mit dem ersten Poll konkurriert. */
const UPDATE_CHECK_DELAY_MS = 60 * 1000; const UPDATE_CHECK_DELAY_MS = 60 * 1000;
@@ -37,6 +60,7 @@ const UPDATE_CHECK_DELAY_MS = 60 * 1000;
let win = null; let win = null;
let settingsWin = null; let settingsWin = null;
let errorsWin = null; let errorsWin = null;
let infoWin = null;
let tray = null; let tray = null;
let collector = null; let collector = null;
let store = null; let store = null;
@@ -248,6 +272,14 @@ function buildTrayMenu() {
}, },
{ type: 'separator' }, { type: 'separator' },
{ label: 'Einstellungen …', click: openSettings }, { label: 'Einstellungen …', click: openSettings },
{
label: 'Info',
submenu: [
{ label: 'Benutzerhandbuch', click: () => openInfoDoc('manual') },
{ label: 'Disclaimer', click: () => openInfoDoc('disclaimer') },
{ label: 'Lizenz', click: () => openInfoDoc('license') },
],
},
{ label: 'Datenordner öffnen', click: () => shell.openPath(USER_DATA_DIR) }, { label: 'Datenordner öffnen', click: () => shell.openPath(USER_DATA_DIR) },
{ type: 'separator' }, { type: 'separator' },
{ label: 'Beenden', click: () => app.quit() }, { label: 'Beenden', click: () => app.quit() },
@@ -364,6 +396,152 @@ function openErrorLog() {
}); });
} }
/** Escaped Text für die Einbettung in HTML — nur für die Fehlermeldung unten. */
function escapeHtml(text) {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/** GitHub-ähnlicher Anker-Slug — reicht für die deutschen Überschriften hier
* (Umlaute bleiben erhalten, nur Satzzeichen fallen weg). */
function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
}
/**
* Markdown→HTML mit `id`-Attributen auf jeder Überschrift, damit
* Sprunganker (z. B. aus einem Inhaltsverzeichnis) funktionieren — marked
* vergibt seit Version 5 selbst keine Heading-IDs mehr (früher `headerIds`,
* jetzt eine separate Erweiterung, die wir nicht zusätzlich einbinden).
* Ein frischer Renderer pro Aufruf hält die Dopplungs-Zählung dokumentweise
* sauber getrennt.
*/
function renderMarkdown(raw) {
const renderer = new marked.Renderer();
const seen = new Map();
renderer.heading = function ({ tokens, depth, text }) {
const inline = this.parser.parseInline(tokens);
let slug = slugify(text) || 'abschnitt';
const count = seen.get(slug) || 0;
seen.set(slug, count + 1);
if (count > 0) slug = `${slug}-${count}`;
return `<h${depth} id="${slug}">${inline}</h${depth}>\n`;
};
return marked.parse(raw, { renderer });
}
/**
* Behandelt einen Link-Klick im Info-Dialog. Ohne dieses Abfangen navigiert
* Electron das Fenster selbst zum verlinkten Ziel — bei einem relativen Link
* auf eine .md-Datei bleibt das Fenster dann leer und ohne Titelleiste hängen
* (kein Schließen-Button mehr). Externe http(s)-Links öffnen im Standard-
* browser, interne Verweise auf die drei bekannten Dokumente wechseln den
* Inhalt desselben Dialogs; alles andere wird verworfen.
*/
function handleInfoLink(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
return;
}
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
shell.openExternal(url);
return;
}
if (parsed.protocol === 'file:') {
const base = path.basename(decodeURIComponent(parsed.pathname));
const kind = INFO_LINK_TARGETS[base];
if (kind) openInfoDoc(kind);
}
}
/**
* 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 = renderMarkdown(raw);
} catch (err) {
html = `<p>Datei konnte nicht gelesen werden: ${escapeHtml(err.message)}</p>`;
}
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,
},
});
// Linkklicks im gerenderten Markdown nie im Fenster selbst navigieren
// lassen (siehe handleInfoLink()) — sonst bleibt ein leeres, unschließbares
// Fenster zurück.
infoWin.webContents.on('will-navigate', (event, url) => {
// Reiner Sprunganker innerhalb desselben Dokuments (z. B. aus dem
// Inhaltsverzeichnis) — Standardverhalten (Scrollen zum Anker) zulassen,
// statt es wie eine echte Navigation zu behandeln.
const currentBase = infoWin.webContents.getURL().split('#')[0];
if (url.split('#')[0] === currentBase) return;
event.preventDefault();
handleInfoLink(url);
});
infoWin.webContents.setWindowOpenHandler(({ url }) => {
handleInfoLink(url);
return { action: 'deny' };
});
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 * Übernimmt geänderte Einstellungen ohne Neustart und schreibt sie in die
* Datei. Unbekannte Felder und die `_`-Kommentare bleiben erhalten, weil die * Datei. Unbekannte Felder und die `_`-Kommentare bleiben erhalten, weil die
@@ -419,6 +597,10 @@ function registerIpc() {
const info = collector?.state?.update; const info = collector?.state?.update;
if (info?.available && info.htmlUrl) shell.openExternal(info.htmlUrl); if (info?.available && info.htmlUrl) shell.openExternal(info.htmlUrl);
}); });
ipcMain.on('open-manual', () => openInfoDoc('manual'));
ipcMain.on('open-disclaimer', () => openInfoDoc('disclaimer'));
ipcMain.on('open-license', () => openInfoDoc('license'));
ipcMain.on('info:close', () => infoWin && infoWin.close());
ipcMain.handle('settings:load', () => ({ ipcMain.handle('settings:load', () => ({
config, config,
+6
View File
@@ -26,4 +26,10 @@ contextBridge.exposeInMainWorld('dashboard', {
openErrorLog: () => ipcRenderer.send('open-error-log'), openErrorLog: () => ipcRenderer.send('open-error-log'),
/** Öffnet die Release-Seite eines gefundenen neuen Updates im Browser. */ /** Öffnet die Release-Seite eines gefundenen neuen Updates im Browser. */
openReleasePage: () => ipcRenderer.send('open-release-page'), openReleasePage: () => ipcRenderer.send('open-release-page'),
/** Öffnet den Benutzerhandbuch-Dialog. */
openManual: () => ipcRenderer.send('open-manual'),
/** Öffnet den Disclaimer-Dialog. */
openDisclaimer: () => ipcRenderer.send('open-disclaimer'),
/** Öffnet den Lizenz-Dialog. */
openLicense: () => ipcRenderer.send('open-license'),
}); });
+32
View File
@@ -48,6 +48,12 @@ const el = {
staleBadge: $('stale-badge'), staleBadge: $('stale-badge'),
btnEndpointStatus: $('btn-endpoint-status'), btnEndpointStatus: $('btn-endpoint-status'),
btnUpdateAvailable: $('btn-update-available'), btnUpdateAvailable: $('btn-update-available'),
infoWrap: $('info-wrap'),
btnInfo: $('btn-info'),
infoMenu: $('info-menu'),
infoMenuManual: $('info-menu-manual'),
infoMenuDisclaimer: $('info-menu-disclaimer'),
infoMenuLicense: $('info-menu-license'),
btnHide: $('btn-hide'), btnHide: $('btn-hide'),
btnSettings: $('btn-settings'), btnSettings: $('btn-settings'),
btnExpand: $('btn-expand'), btnExpand: $('btn-expand'),
@@ -475,6 +481,32 @@ el.btnSettings.addEventListener('click', () => window.dashboard.openSettings());
el.btnEndpointStatus.addEventListener('click', () => window.dashboard.openErrorLog()); el.btnEndpointStatus.addEventListener('click', () => window.dashboard.openErrorLog());
el.btnUpdateAvailable.addEventListener('click', () => window.dashboard.openReleasePage()); 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();
});
el.infoMenuLicense.addEventListener('click', () => {
el.infoMenu.hidden = true;
window.dashboard.openLicense();
});
window.dashboard.onState(render); window.dashboard.onState(render);
// Reihenfolge: erst die Ansicht auf die wiederhergestellte Fenstergröße // Reihenfolge: erst die Ansicht auf die wiederhergestellte Fenstergröße
+8
View File
@@ -30,6 +30,14 @@
/> />
</svg> </svg>
</button> </button>
<div class="info-wrap" id="info-wrap">
<button class="icon-btn" id="btn-info" title="Info" aria-label="Info"></button>
<div class="info-menu" id="info-menu" hidden>
<button class="info-menu-item" id="info-menu-manual">Benutzerhandbuch</button>
<button class="info-menu-item" id="info-menu-disclaimer">Disclaimer</button>
<button class="info-menu-item" id="info-menu-license">Lizenz</button>
</div>
</div>
<button class="icon-btn" id="btn-hide" title="Ausblenden (über das Tray-Symbol zurückholen)"></button> <button class="icon-btn" id="btn-hide" title="Ausblenden (über das Tray-Symbol zurückholen)"></button>
</header> </header>
+41
View File
@@ -157,6 +157,47 @@ body {
color: var(--status-update); 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 ───────────────────────────────────────────────────────────── */ /* ── Meter ───────────────────────────────────────────────────────────── */
.meters { .meters {