Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd29e00707 | ||
|
|
378e2f86e5 | ||
|
|
fff2f288cb | ||
|
|
f8a286f219 | ||
|
|
bb7089f560 | ||
|
|
dd560a36cb | ||
|
|
011e5b6816 | ||
|
|
a01f4266ea | ||
|
|
39f19ab261 |
+39
-2
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
unten mehr unter „Datenschutz und Sicherheit“.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Windows 10/11 oder Ubuntu (bzw. eine vergleichbare Linux-Distribution)
|
||||
- Claude Code muss installiert sein und mindestens einmal benutzt worden sein
|
||||
— ohne vorhandene Protokolldateien hat das Dashboard nichts anzuzeigen
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Installation
|
||||
|
||||
### 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
|
||||
trotzdem, nur ohne sichtbares Symbol in der oberen Leiste.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Erster Start
|
||||
|
||||
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
|
||||
heften.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Die Oberfläche
|
||||
|
||||
**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 |
|
||||
| Pfeil-Symbol (nur bei neuem Release) | Öffnet die Release-Seite im Browser |
|
||||
| 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) |
|
||||
|
||||
**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
|
||||
dieses Log meist der schnellste Weg herauszufinden, was los ist.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Einstellungen
|
||||
|
||||
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
|
||||
Dialog ohne zu speichern.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Tray-Menü
|
||||
|
||||
Ein Rechtsklick (Linux) bzw. Klick (Windows) auf das Symbol im
|
||||
Infobereich/der Systemleiste öffnet ein Menü mit: Dashboard anzeigen/ausblenden,
|
||||
Deckkraft, Klick-Durchlässigkeit, Autostart, Einstellungen, Datenordner öffnen
|
||||
und Beenden.
|
||||
Deckkraft, Klick-Durchlässigkeit, Autostart, Einstellungen, Info
|
||||
(Benutzerhandbuch/Disclaimer/Lizenz), Datenordner öffnen und Beenden.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Datenschutz und Sicherheit
|
||||
|
||||
@@ -168,6 +199,8 @@ und Beenden.
|
||||
Zugriffstoken und ohne weitere Daten. Abschaltbar in den Einstellungen.
|
||||
- Es findet keine Übertragung an Dritte statt.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Deinstallation
|
||||
|
||||
**Windows:** über *Einstellungen ▸ Apps* bzw. den mitinstallierten
|
||||
@@ -178,6 +211,8 @@ erneut installieren.
|
||||
**Linux:** `.deb`-Installation mit `sudo apt remove claude-live-dashboard`;
|
||||
ein AppImage genügt zu löschen.
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
## Häufige Fragen
|
||||
|
||||
**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
|
||||
neuere Version installiert wurde. Abschaltbar über die Einstellung „Nach
|
||||
Updates suchen".
|
||||
|
||||
[nach oben](#inhalt)
|
||||
|
||||
+103
@@ -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.
|
||||
@@ -75,6 +75,7 @@ ELECTRON_OZONE_PLATFORM_HINT=wayland ./start.sh # nativ Wayland, ohne Position
|
||||
| Kopfzeile ziehen | Fenster verschieben (Position wird gemerkt) |
|
||||
| Statuspunkt (links vom Zahnrad) | Fehler-/Ereignis-Log öffnen |
|
||||
| ⚙ | Einstellungen öffnen |
|
||||
| ⓘ | Menü mit Benutzerhandbuch/Disclaimer/Lizenz — öffnet den jeweiligen Text in einem eigenen Fenster |
|
||||
| „Details“ | Modell-Aufschlüsselung, Tempo und Datengrundlage aufklappen |
|
||||
| `–` | Ausblenden — Rückkehr über das Tray-Symbol |
|
||||
| Tray-Klick | Ein-/Ausblenden |
|
||||
@@ -175,6 +176,10 @@ Fenster bekannt — daraus folgt die Bezugsgröße unmittelbar. Auch das
|
||||
Wochenfenster wird aus dem gemeldeten Reset-Zeitpunkt abgeleitet, nicht
|
||||
geraten.
|
||||
|
||||
Der erste automatische Kalibrierungsschritt einer Sitzung erscheint als
|
||||
Eintrag im Ereignisprotokoll ("Automatisch kalibriert …") — spätere
|
||||
Aktualisierungen laufen bewusst lautlos im Hintergrund weiter.
|
||||
|
||||
Die folgenden Abschnitte betreffen nur den Fall, dass die Abfrage **nicht**
|
||||
verfügbar ist.
|
||||
|
||||
|
||||
Generated
+1090
-1999
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "claude-live-dashboard",
|
||||
"productName": "Claude Live Dashboard",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"private": true,
|
||||
"description": "Always-on-Top Mini-Dashboard für Claude-Plan-Auslastung unter Windows und Linux",
|
||||
"main": "src/main/index.js",
|
||||
@@ -21,7 +21,7 @@
|
||||
"homepage": "https://github.com/exilkieler/claude-live-dashboard",
|
||||
"devDependencies": {
|
||||
"electron": "^43.2.0",
|
||||
"electron-builder": "^25.1.8"
|
||||
"electron-builder": "^26.15.3"
|
||||
},
|
||||
"build": {
|
||||
"appId": "de.local.claude-live-dashboard",
|
||||
@@ -35,6 +35,9 @@
|
||||
"src/**/*",
|
||||
"assets/**/*",
|
||||
"config.json",
|
||||
"BENUTZERHANDBUCH.md",
|
||||
"DISCLAIMER.md",
|
||||
"LICENSE.txt",
|
||||
"!**/*.map"
|
||||
],
|
||||
"win": {
|
||||
@@ -93,9 +96,14 @@
|
||||
"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.",
|
||||
"desktop": {
|
||||
"StartupWMClass": "claude-live-dashboard"
|
||||
"entry": {
|
||||
"StartupWMClass": "claude-live-dashboard"
|
||||
}
|
||||
},
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "^18.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
+17
-3
@@ -82,6 +82,10 @@ class Collector extends EventEmitter {
|
||||
// wie die Notification (store.data.updateCheck.notifiedTag), das Symbol
|
||||
// soll sichtbar bleiben, bis aktualisiert wird.
|
||||
this._updateAvailable = null;
|
||||
// Merkt sich pro Woche/Block, ob in dieser Sitzung bereits eine
|
||||
// Kalibrierung aus einer Serverantwort übernommen wurde — für eine
|
||||
// einmalige Rückmeldung im Ereignisprotokoll, siehe _rememberServerLimit().
|
||||
this._calibratedOnce = { week: false, block: false };
|
||||
if (configError) this._note(configError, 'local');
|
||||
if (versionEvent) {
|
||||
const msg =
|
||||
@@ -283,6 +287,12 @@ class Collector extends EventEmitter {
|
||||
if (!previous || Math.abs(next - previous) / previous > 0.005) {
|
||||
store.serverLimits[kind] = Math.round(next);
|
||||
this.store.save();
|
||||
|
||||
if (!this._calibratedOnce[kind]) {
|
||||
this._calibratedOnce[kind] = true;
|
||||
const label = kind === 'week' ? 'Woche' : 'Zeitfenster';
|
||||
this._note(`Automatisch kalibriert (${label}) aus Serverantwort`, 'oauth');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,13 +330,17 @@ class Collector extends EventEmitter {
|
||||
const block = this.limits.evaluate('block', snapshot.block.weighted, server?.blockLimit);
|
||||
const week = this.limits.evaluate('week', snapshot.week.weighted, server?.weekLimit);
|
||||
|
||||
// Ein exakter Prozentwert vom Server sticht die lokale Rechnung.
|
||||
// Ein exakter Prozentwert vom Server sticht die lokale Rechnung. Der
|
||||
// Endpunkt liefert bereits einen echten Prozentwert (0–100), nicht einen
|
||||
// Bruch — eine frühere "<=1 → *100"-Heuristik verfälschte z. B. 1 % zu
|
||||
// 100 %, siehe _rememberServerLimit() unten, die weekPercent schon immer
|
||||
// konsistent als 0–100-Wert behandelt.
|
||||
if (server?.blockPercent != null) {
|
||||
block.percent = server.blockPercent <= 1 ? server.blockPercent * 100 : server.blockPercent;
|
||||
block.percent = server.blockPercent;
|
||||
block.confidence = 'exact';
|
||||
}
|
||||
if (server?.weekPercent != null) {
|
||||
week.percent = server.weekPercent <= 1 ? server.weekPercent * 100 : server.weekPercent;
|
||||
week.percent = server.weekPercent;
|
||||
week.confidence = 'exact';
|
||||
}
|
||||
|
||||
|
||||
@@ -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,28 @@ 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 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
|
||||
* Start nicht verlangsamt und nicht mit dem ersten Poll konkurriert. */
|
||||
const UPDATE_CHECK_DELAY_MS = 60 * 1000;
|
||||
@@ -37,6 +60,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;
|
||||
@@ -248,6 +272,14 @@ function buildTrayMenu() {
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ 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) },
|
||||
{ type: 'separator' },
|
||||
{ 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/** 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
|
||||
* Datei. Unbekannte Felder und die `_`-Kommentare bleiben erhalten, weil die
|
||||
@@ -419,6 +597,10 @@ 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('open-license', () => openInfoDoc('license'));
|
||||
ipcMain.on('info:close', () => infoWin && infoWin.close());
|
||||
|
||||
ipcMain.handle('settings:load', () => ({
|
||||
config,
|
||||
|
||||
@@ -26,4 +26,10 @@ 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'),
|
||||
/** Öffnet den Lizenz-Dialog. */
|
||||
openLicense: () => ipcRenderer.send('open-license'),
|
||||
});
|
||||
|
||||
@@ -48,6 +48,12 @@ 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'),
|
||||
infoMenuLicense: $('info-menu-license'),
|
||||
btnHide: $('btn-hide'),
|
||||
btnSettings: $('btn-settings'),
|
||||
btnExpand: $('btn-expand'),
|
||||
@@ -475,6 +481,32 @@ 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();
|
||||
});
|
||||
el.infoMenuLicense.addEventListener('click', () => {
|
||||
el.infoMenu.hidden = true;
|
||||
window.dashboard.openLicense();
|
||||
});
|
||||
|
||||
window.dashboard.onState(render);
|
||||
|
||||
// Reihenfolge: erst die Ansicht auf die wiederhergestellte Fenstergröße
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user