Endpunkt-Status-Icon mit Ereignis-Log und Update-Erkennung ergaenzen
Ein neues Status-Icon im Titelbalken zeigt den Zustand der lokalen Transkripte und der Serverabfrage; ein Klick oeffnet ein eigenes Fenster mit persistiertem, zeitgestempeltem Ereignis-/Fehlerprotokoll (loeschbar). Ausserdem erkennt die App beim Start Erstinstallation vs. Update (plattformunabhaengig fuer Windows und Linux) und meldet Updates per nativer Benachrichtigung. Dazu: Versionierungs-Policy dokumentiert (package.json vor jedem Release erhoehen, sonst greift weder die Update-Erkennung noch bleiben alte Release-Dateien in release/ erhalten), sowie zwei neue Dokumente fuer Endanwender (BENUTZERHANDBUCH.md) und eine Download-Webseite (PRODUKTSEITE.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c1b899ec8b
commit
e149a9118f
@@ -0,0 +1,181 @@
|
|||||||
|
# Claude Live Dashboard — Benutzerhandbuch
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Was die Anwendung tut
|
||||||
|
|
||||||
|
Claude Live Dashboard ist ein kleines, immer sichtbares Fenster, das anzeigt,
|
||||||
|
wie stark Ihr Claude-Abo gerade ausgelastet ist: das laufende 5-Stunden-Fenster,
|
||||||
|
das Wochenkontingent, der Zeitpunkt der nächsten Zurücksetzung und — sofern Ihr
|
||||||
|
Abo es ausweist — das verbleibende Nutzungsguthaben.
|
||||||
|
|
||||||
|
Die Anzeige liest dafür ausschließlich die Protokolldateien, die Claude Code
|
||||||
|
auf Ihrem Rechner ohnehin selbst anlegt. Es wird nichts zusätzlich
|
||||||
|
mitgeschnitten. Optional (und standardmäßig aktiv) fragt die Anwendung
|
||||||
|
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“.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Zwei Varianten stehen zur Wahl:
|
||||||
|
|
||||||
|
- **Installer** (`Claude Live Dashboard-<Version>-x64.exe`): Doppelklick
|
||||||
|
starten, Installationsordner bestätigen oder ändern, fertig. Es werden eine
|
||||||
|
Desktop- und eine Startmenü-Verknüpfung angelegt. Administratorrechte sind
|
||||||
|
**nicht** nötig, die Installation erfolgt in Ihr Benutzerprofil.
|
||||||
|
- **Portable** (`Claude Live Dashboard-<Version>-portable.exe`): läuft ohne
|
||||||
|
Installation direkt von jedem Ort, z. B. einem USB-Stick.
|
||||||
|
|
||||||
|
Da die Anwendung nicht mit einem kostenpflichtigen Code-Signing-Zertifikat
|
||||||
|
signiert ist, zeigt Windows beim allerersten Start eine SmartScreen-Warnung
|
||||||
|
(„Der Computer wurde durch Windows geschützt“). Über **Weitere Informationen**
|
||||||
|
und dann **Trotzdem ausführen** lässt sie sich bestätigen. Das ist normal für
|
||||||
|
Software ohne kommerzielles Zertifikat und kein Hinweis auf ein Problem.
|
||||||
|
|
||||||
|
### Linux (Ubuntu und verwandte Distributionen)
|
||||||
|
|
||||||
|
- **`.deb`-Paket**: `sudo apt install ./Claude-Live-Dashboard-<Version>-amd64.deb`
|
||||||
|
— richtet Menüeintrag und Symbol automatisch ein.
|
||||||
|
- **AppImage**: Datei ausführbar machen (`chmod +x Claude-Live-Dashboard-<Version>-x64.AppImage`)
|
||||||
|
und starten. Setzt `libfuse2` voraus; unter Ubuntu 24.04 ist das ggf.
|
||||||
|
nachzuinstallieren (`sudo apt install libfuse2t64` oder `libfuse2`, je nach
|
||||||
|
Version).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Erster Start
|
||||||
|
|
||||||
|
Nach dem Start erscheint das Fenster rechts oben auf dem Bildschirm und bleibt
|
||||||
|
über allen anderen Fenstern sichtbar. Zusätzlich landet ein Symbol im
|
||||||
|
Infobereich der Taskleiste (Windows) bzw. in der oberen Systemleiste (Linux).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Die Oberfläche
|
||||||
|
|
||||||
|
**Kopfzeile** — per Ziehen lässt sich das Fenster frei positionieren, die
|
||||||
|
Position wird gemerkt.
|
||||||
|
|
||||||
|
| Element | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| Statuspunkt | Zeigt den Zustand der Datenquellen, siehe unten |
|
||||||
|
| Zahnrad | Öffnet die Einstellungen |
|
||||||
|
| `–` | Blendet das Fenster aus (Rückkehr über das Tray-Symbol) |
|
||||||
|
|
||||||
|
**Anzeigebereich**
|
||||||
|
|
||||||
|
- **Laufende Sitzung** — Auslastung des aktuellen 5-Stunden-Fensters, dazu
|
||||||
|
verbrauchte Tokens und Verbrauchstempo.
|
||||||
|
- **Wochenkontingent** — das übergeordnete Limit über alle Modelle, mit dem
|
||||||
|
Zeitpunkt der nächsten Zurücksetzung.
|
||||||
|
- **Nutzungsguthaben** — erscheint nur, wenn Ihr Abo ein zusätzliches
|
||||||
|
Geldbudget führt.
|
||||||
|
- Ein Satz darunter sagt, wie lange das Wochenkontingent bei aktuellem Tempo
|
||||||
|
noch reicht — gerechnet in tatsächlicher Arbeitszeit, nicht in Kalendertagen.
|
||||||
|
|
||||||
|
**„Details“** klappt eine erweiterte Ansicht auf: Verbrauch je Modell, Tempo,
|
||||||
|
und wie verlässlich die angezeigten Prozentwerte sind.
|
||||||
|
|
||||||
|
### Endpunkt-Status und Fehler-/Ereignis-Log
|
||||||
|
|
||||||
|
Der Punkt links vom Zahnrad zeigt auf einen Blick, ob alles rund läuft:
|
||||||
|
|
||||||
|
| Aussehen | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| Grüner Punkt | Alles in Ordnung |
|
||||||
|
| Gelbe Raute | Die optionale Serverabfrage ist gerade gestört — die Anzeige rechnet währenddessen lokal weiter, ist also weiterhin nutzbar, nur mit geschätzten statt exakten Werten |
|
||||||
|
| Rote Raute | Die lokalen Protokolldateien sind nicht lesbar — das ist die eigentliche Datengrundlage, hier lohnt ein Blick ins Log |
|
||||||
|
|
||||||
|
Ein Klick auf den Punkt öffnet ein eigenes Fenster mit einer Liste aller
|
||||||
|
bisherigen Störungen samt Zeitpunkt, sowie App-Ereignissen wie „App
|
||||||
|
installiert“ oder „Auf Version … aktualisiert“. Diese Liste bleibt auch über
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Einstellungen
|
||||||
|
|
||||||
|
Das Zahnrad öffnet den Einstellungsdialog:
|
||||||
|
|
||||||
|
- **Deckkraft** — wie durchscheinend das Fenster ist.
|
||||||
|
- **Klicks durchlassen** — macht das Fenster für die Maus „unsichtbar“, es
|
||||||
|
liegt dann über der Arbeit, ohne sie zu stören. Zurückschalten geht über das
|
||||||
|
Tray-Menü (siehe unten), da das Fenster in diesem Zustand selbst nicht mehr
|
||||||
|
anklickbar ist.
|
||||||
|
- **Mit Windows/dem System starten** — startet die Anwendung automatisch bei
|
||||||
|
der Anmeldung.
|
||||||
|
- **Serverabfrage** — kann abgeschaltet werden, falls die Anzeige rein lokal
|
||||||
|
arbeiten soll (dann sind alle Werte Näherungen statt exakter Zahlen).
|
||||||
|
- **Bezugsgrößen, Gewichtung, Wochenfenster** — fortgeschrittene Feineinstellungen
|
||||||
|
für den Fall, dass die Serverabfrage dauerhaft nicht zur Verfügung steht;
|
||||||
|
im Normalbetrieb müssen Sie hier nichts anpassen.
|
||||||
|
|
||||||
|
Änderungen wirken sofort, ein Neustart ist nicht nötig. `Strg`+`S` speichert,
|
||||||
|
`Esc` schließt den Dialog ohne zu speichern.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Datenschutz und Sicherheit
|
||||||
|
|
||||||
|
- Die Anwendung liest ausschließlich Dateien, die Claude Code selbst bereits
|
||||||
|
auf Ihrem Rechner ablegt. Es wird nichts zusätzlich protokolliert.
|
||||||
|
- Standardmäßig fragt sie zusätzlich einen offiziellen Anthropic-Endpunkt ab,
|
||||||
|
um exakte statt geschätzter Prozentwerte anzuzeigen. Dafür wird dasselbe
|
||||||
|
Zugriffstoken verwendet, das Claude Code selbst benutzt — es wird gelesen,
|
||||||
|
aber nie gespeichert, nie protokolliert und ausschließlich an diesen einen,
|
||||||
|
offiziellen Host gesendet.
|
||||||
|
- Diese Serverabfrage lässt sich in den Einstellungen jederzeit abschalten;
|
||||||
|
die Anwendung funktioniert dann weiter, nur mit geschätzten statt exakten
|
||||||
|
Werten.
|
||||||
|
- Es findet keine Übertragung an Dritte statt.
|
||||||
|
|
||||||
|
## Deinstallation
|
||||||
|
|
||||||
|
**Windows:** über *Einstellungen ▸ Apps* bzw. den mitinstallierten
|
||||||
|
Deinstallierer im Installationsordner. Ihre Konfiguration und die
|
||||||
|
Kalibrierungsdaten bleiben dabei erhalten, falls Sie die Anwendung später
|
||||||
|
erneut installieren.
|
||||||
|
|
||||||
|
**Linux:** `.deb`-Installation mit `sudo apt remove claude-live-dashboard`;
|
||||||
|
ein AppImage genügt zu löschen.
|
||||||
|
|
||||||
|
## Häufige Fragen
|
||||||
|
|
||||||
|
**Das Fenster zeigt keine Zahlen an.** Vermutlich wurde Claude Code auf diesem
|
||||||
|
Rechner noch nicht benutzt — die Anzeige braucht mindestens einen
|
||||||
|
protokollierten Verlauf, um etwas darzustellen.
|
||||||
|
|
||||||
|
**Der Statuspunkt ist rot.** Die lokalen Protokolldateien konnten nicht
|
||||||
|
gelesen werden. Ein Klick auf den Punkt zeigt im Log den genauen Grund.
|
||||||
|
|
||||||
|
**Der Statuspunkt ist gelb.** Nur die optionale Serverabfrage ist gerade
|
||||||
|
gestört, meist vorübergehend (z. B. Netzwerkproblem oder ein abgelaufenes
|
||||||
|
Zugriffstoken, das Claude Code beim nächsten eigenen Start erneuert). Die
|
||||||
|
Anzeige selbst funktioniert währenddessen weiter.
|
||||||
|
|
||||||
|
**Windows warnt beim ersten Start.** Normal bei unsignierter Software, siehe
|
||||||
|
Abschnitt „Installation“ oben.
|
||||||
|
|
||||||
|
**Nach einem Update sehe ich eine Benachrichtigung.** Das ist beabsichtigt:
|
||||||
|
Die Anwendung erkennt neue Versionen automatisch und weist einmalig darauf
|
||||||
|
hin. Ihre Einstellungen und Kalibrierung bleiben davon unberührt.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Claude Live Dashboard
|
||||||
|
|
||||||
|
**Immer im Blick, wie stark Ihr Claude-Abo gerade ausgelastet ist — ohne
|
||||||
|
einen Tab zu wechseln.**
|
||||||
|
|
||||||
|
Claude Live Dashboard ist ein schlankes, immer sichtbares Widget für Windows
|
||||||
|
und Linux, das die Auslastung Ihres Claude-Abos live anzeigt: das laufende
|
||||||
|
5-Stunden-Fenster, das Wochenkontingent, den Zeitpunkt der nächsten
|
||||||
|
Zurücksetzung und — je nach Abo — das verbleibende Nutzungsguthaben. Dazu die
|
||||||
|
eine Zahl, die eigentlich zählt: wie lange das Kontingent beim aktuellen
|
||||||
|
Arbeitstempo noch reicht.
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
- **5-Stunden-Fenster und Wochenkontingent** auf einen Blick, inklusive
|
||||||
|
Reset-Countdown
|
||||||
|
- **Exakte Werte statt Schätzungen** — fragt optional denselben Endpunkt ab,
|
||||||
|
den auch `/usage` in Claude Code nutzt
|
||||||
|
- **Verbrauch je Modell** in der aufklappbaren Detailansicht
|
||||||
|
- **Hochrechnung in aktiver Arbeitszeit**, nicht in Kalendertagen — sagt, wie
|
||||||
|
lange das Kontingent bei diesem Tempo noch trägt
|
||||||
|
- **Endpunkt-Status-Anzeige** mit chronologischem Ereignis-Log, damit auf
|
||||||
|
einen Blick klar ist, ob alle Datenquellen funktionieren
|
||||||
|
- **Automatische Update-Erkennung** mit einmaliger Benachrichtigung nach
|
||||||
|
einem Update
|
||||||
|
- **Läuft lokal** — liest nur, was Claude Code ohnehin bereits auf Ihrem
|
||||||
|
Rechner protokolliert; die optionale Serverabfrage lässt sich jederzeit
|
||||||
|
abschalten
|
||||||
|
- Frei positionierbar, transparent, mit einstellbarer Deckkraft und optional
|
||||||
|
klickdurchlässig, damit es nie im Weg ist
|
||||||
|
- Windows und Linux (Ubuntu und verwandte Distributionen), inklusive
|
||||||
|
Wayland-Unterstützung
|
||||||
|
|
||||||
|
## Systemvoraussetzungen
|
||||||
|
|
||||||
|
- Windows 10/11 oder Ubuntu (bzw. eine vergleichbare Linux-Distribution)
|
||||||
|
- Eine bestehende Claude-Code-Installation, aus deren lokalen Protokollen die
|
||||||
|
Anzeige ihre Daten bezieht
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
| Datei | Plattform | Hinweis |
|
||||||
|
|---|---|---|
|
||||||
|
| `Claude Live Dashboard-<Version>-x64.exe` | Windows | Installer, ohne Administratorrechte |
|
||||||
|
| `Claude Live Dashboard-<Version>-portable.exe` | Windows | ohne Installation, direkt lauffähig |
|
||||||
|
| `Claude Live Dashboard-<Version>-amd64.deb` | Linux (Debian/Ubuntu) | `sudo apt install ./…deb` |
|
||||||
|
| `Claude Live Dashboard-<Version>-x64.AppImage` | Linux | ohne Installation, `chmod +x` genügt |
|
||||||
|
|
||||||
|
Ausführliche Installationsschritte und die Bedienung im Detail: siehe
|
||||||
|
Benutzerhandbuch.
|
||||||
|
|
||||||
|
## Datenschutz zuerst
|
||||||
|
|
||||||
|
Es wird nichts über das hinaus protokolliert, was Claude Code selbst bereits
|
||||||
|
auf Ihrem Rechner ablegt, und nichts an Dritte übertragen. Die einzige
|
||||||
|
optionale Netzwerkverbindung führt zu einem offiziellen Anthropic-Endpunkt,
|
||||||
|
um statt geschätzter exakte Werte anzuzeigen — mit dem ohnehin vorhandenen
|
||||||
|
Zugriffstoken, das nie gespeichert oder protokolliert wird. Diese Abfrage
|
||||||
|
lässt sich jederzeit abschalten, ohne dass die Anzeige ihre Funktion verliert.
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT — freie Nutzung, Quellcode einsehbar.
|
||||||
@@ -73,6 +73,7 @@ ELECTRON_OZONE_PLATFORM_HINT=wayland ./start.sh # nativ Wayland, ohne Position
|
|||||||
| Aktion | Wirkung |
|
| Aktion | Wirkung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Kopfzeile ziehen | Fenster verschieben (Position wird gemerkt) |
|
| Kopfzeile ziehen | Fenster verschieben (Position wird gemerkt) |
|
||||||
|
| Statuspunkt (links vom Zahnrad) | Fehler-/Ereignis-Log öffnen |
|
||||||
| ⚙ | Einstellungen öffnen |
|
| ⚙ | Einstellungen öffnen |
|
||||||
| „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 |
|
||||||
@@ -98,6 +99,23 @@ sofort beim Umschalten übernommen, unabhängig vom Speichern.
|
|||||||
dann über der Arbeit, ohne sie zu behindern. Zurückschalten geht über das
|
dann über der Arbeit, ohne sie zu behindern. Zurückschalten geht über das
|
||||||
Tray-Menü.
|
Tray-Menü.
|
||||||
|
|
||||||
|
### Endpunkt-Status & Ereignis-Log
|
||||||
|
|
||||||
|
Der Punkt links vom Zahnrad zeigt den Zustand der beiden Datenquellen —
|
||||||
|
Symbol *und* Farbe, nie Farbe allein:
|
||||||
|
|
||||||
|
| Zustand | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| ● grün | Beide Quellen funktionieren |
|
||||||
|
| ◆ gelb | Serverabfrage gestört (App rechnet lokal weiter) |
|
||||||
|
| ◆ rot | Lokale Transkripte nicht lesbar — die Kerndatenquelle fehlt |
|
||||||
|
|
||||||
|
Ein Klick öffnet ein eigenes Fenster mit dem chronologischen, zeitgestempelten
|
||||||
|
Protokoll aller Störungen (Lesefehler, Dateiüberwachung, Serverabfrage) sowie
|
||||||
|
App-Ereignissen wie Erstinstallation und Updates. Das Protokoll übersteht
|
||||||
|
Neustarts und das Speichern der Einstellungen — es liegt in `state.json` — und
|
||||||
|
lässt sich über „Log löschen“ im selben Fenster zurücksetzen.
|
||||||
|
|
||||||
## Was angezeigt wird
|
## Was angezeigt wird
|
||||||
|
|
||||||
**Laufende Sitzung** — Auslastung des aktuellen 5-Stunden-Fensters, dazu
|
**Laufende Sitzung** — Auslastung des aktuellen 5-Stunden-Fensters, dazu
|
||||||
@@ -206,8 +224,10 @@ src/main/paths.js Speicherorte für Konfiguration und Zustand
|
|||||||
src/main/autostart.js Anmeldestart je Betriebssystem
|
src/main/autostart.js Anmeldestart je Betriebssystem
|
||||||
src/preload.js contextBridge fürs Widget
|
src/preload.js contextBridge fürs Widget
|
||||||
src/settings-preload.js contextBridge für den Einstellungsdialog
|
src/settings-preload.js contextBridge für den Einstellungsdialog
|
||||||
|
src/errors-preload.js contextBridge fürs Fehler-/Ereignis-Log
|
||||||
src/renderer/ Widget-Oberfläche
|
src/renderer/ Widget-Oberfläche
|
||||||
src/settings/ Einstellungsdialog
|
src/settings/ Einstellungsdialog
|
||||||
|
src/errors/ Fehler-/Ereignis-Log-Fenster
|
||||||
```
|
```
|
||||||
|
|
||||||
Alle Dateizugriffe und Rechnungen passieren im Main-Prozess; der Renderer
|
Alle Dateizugriffe und Rechnungen passieren im Main-Prozess; der Renderer
|
||||||
@@ -223,11 +243,41 @@ Zwei Details, die nicht offensichtlich sind:
|
|||||||
Retries und beim Kompaktieren mehrfach ins Transkript; ohne Dedup über
|
Retries und beim Kompaktieren mehrfach ins Transkript; ohne Dedup über
|
||||||
`message.id` wären alle Zahlen zu hoch.
|
`message.id` wären alle Zahlen zu hoch.
|
||||||
|
|
||||||
|
## Version & Updates
|
||||||
|
|
||||||
|
Die App erkennt beim Start selbst, ob es sich um eine Erstinstallation oder
|
||||||
|
ein Update handelt: Sie vergleicht `app.getVersion()` (aus `package.json`,
|
||||||
|
plattformunabhängig für Windows und Linux) mit der zuletzt in `state.json`
|
||||||
|
gemerkten Version. Bei einer neuen Version erscheint einmalig eine native
|
||||||
|
Systembenachrichtigung, und das Ereignis landet zeitgestempelt im
|
||||||
|
Fehler-/Ereignis-Log (siehe oben). Bestehende Konfiguration und Kalibrierung
|
||||||
|
bleiben davon unberührt — geschrieben wird ausschließlich das eine Feld
|
||||||
|
`state.json:appVersion`.
|
||||||
|
|
||||||
|
**Voraussetzung dafür ist eine bei jedem Release erhöhte Versionsnummer.**
|
||||||
|
Vor jedem Build deshalb zuerst `version` in `package.json` erhöhen (SemVer:
|
||||||
|
`1.1.0` → `1.2.0` für neue Funktionen, `1.1.1` für Fehlerbehebungen). Ohne
|
||||||
|
diesen Schritt erkennt die App den neuen Build nicht als Update, und der
|
||||||
|
Installer überschreibt in `release/` stillschweigend die Datei der
|
||||||
|
vorherigen Version, weil deren Name (`artifactName`) die Versionsnummer
|
||||||
|
enthält:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm version minor # oder: patch / major — schreibt package.json, kein Tag/Commit nötig
|
||||||
|
npm run dist
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm version` legt standardmäßig auch einen Git-Commit und -Tag an; ohne das
|
||||||
|
genügt es, `version` in `package.json` von Hand zu ändern.
|
||||||
|
|
||||||
## Release bauen
|
## Release bauen
|
||||||
|
|
||||||
Gebaut wird jeweils auf dem Zielsystem — electron-builder kann `.deb` nicht
|
Gebaut wird jeweils auf dem Zielsystem — electron-builder kann `.deb` nicht
|
||||||
sinnvoll von Windows aus erzeugen und umgekehrt keine signierten `.exe` unter
|
sinnvoll von Windows aus erzeugen und umgekehrt keine signierten `.exe` unter
|
||||||
Linux.
|
Linux. Beide Linux-Ziele (`AppImage`, `.deb`) brauchen zudem Linux-eigene
|
||||||
|
Werkzeuge (`mksquashfs` bzw. `fpm`) und lassen sich nicht unter Windows
|
||||||
|
erzeugen, auch nicht über WSL ohne eine dort eingerichtete Node.js- bzw.
|
||||||
|
Ruby-Umgebung.
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
@@ -236,12 +286,12 @@ npm run dist # Installer + portable Fassung nach release/
|
|||||||
npm run pack # nur entpackt nach release/win-unpacked (schneller Test)
|
npm run pack # nur entpackt nach release/win-unpacked (schneller Test)
|
||||||
```
|
```
|
||||||
|
|
||||||
Ergebnis in `release/`:
|
Ergebnis in `release/` (Version aus `package.json`, siehe oben):
|
||||||
|
|
||||||
| Datei | Zweck |
|
| Datei | Zweck |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Claude Live Dashboard-1.0.0-x64.exe` | NSIS-Installer, ~97 MB |
|
| `Claude Live Dashboard-<version>-x64.exe` | NSIS-Installer, ~97 MB |
|
||||||
| `Claude Live Dashboard-1.0.0-portable.exe` | läuft ohne Installation |
|
| `Claude Live Dashboard-<version>-portable.exe` | läuft ohne Installation |
|
||||||
|
|
||||||
Der Installer läuft **ohne Administratorrechte** (`perMachine: false`),
|
Der Installer läuft **ohne Administratorrechte** (`perMachine: false`),
|
||||||
installiert also ins Benutzerprofil. Zielverzeichnis ist wählbar, Desktop- und
|
installiert also ins Benutzerprofil. Zielverzeichnis ist wählbar, Desktop- und
|
||||||
@@ -265,8 +315,8 @@ Ergebnis in `release/`:
|
|||||||
|
|
||||||
| Datei | Zweck |
|
| Datei | Zweck |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Claude Live Dashboard-1.0.0-x64.AppImage` | läuft ohne Installation |
|
| `Claude Live Dashboard-<version>-x64.AppImage` | läuft ohne Installation |
|
||||||
| `Claude Live Dashboard-1.0.0-amd64.deb` | Installation per `sudo apt install ./…deb` |
|
| `Claude Live Dashboard-<version>-amd64.deb` | Installation per `sudo apt install ./…deb` |
|
||||||
|
|
||||||
Das AppImage muss einmal ausführbar gemacht werden (`chmod +x`) und braucht
|
Das AppImage muss einmal ausführbar gemacht werden (`chmod +x`) und braucht
|
||||||
`libfuse2`. Es trägt sich nicht selbst ins Anwendungsmenü ein — der Autostart
|
`libfuse2`. Es trägt sich nicht selbst ins Anwendungsmenü ein — der Autostart
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-live-dashboard",
|
"name": "claude-live-dashboard",
|
||||||
"productName": "Claude Live Dashboard",
|
"productName": "Claude Live Dashboard",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Always-on-Top Mini-Dashboard für Claude-Plan-Auslastung unter Windows und Linux",
|
"description": "Always-on-Top Mini-Dashboard für Claude-Plan-Auslastung unter Windows und Linux",
|
||||||
"main": "src/main/index.js",
|
"main": "src/main/index.js",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** Brücke für das Fehler-/Ereignis-Log-Fenster. Bewusst getrennt vom Widget-Preload. */
|
||||||
|
|
||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('errorsBridge', {
|
||||||
|
/** Registriert den Empfänger für Zustandsaktualisierungen. */
|
||||||
|
onState(callback) {
|
||||||
|
ipcRenderer.on('state', (_event, state) => callback(state));
|
||||||
|
},
|
||||||
|
/** Fordert den aktuellen Zustand an (für den ersten Aufbau). */
|
||||||
|
getState: () => ipcRenderer.invoke('get-state'),
|
||||||
|
/** Leert das Ereignisprotokoll. */
|
||||||
|
clear: () => ipcRenderer.send('errors:clear'),
|
||||||
|
/** Schließt das Fenster. */
|
||||||
|
close: () => ipcRenderer.send('errors:close'),
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fehler-/Ereignis-Log. Rechnet nichts nach — Status und Meldungen kommen
|
||||||
|
* fertig aus dem Main-Prozess (`state.endpoints`, `state.errors`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
const el = {
|
||||||
|
close: $('btn-close'),
|
||||||
|
clear: $('btn-clear'),
|
||||||
|
statusLocal: $('status-local'),
|
||||||
|
statusOauth: $('status-oauth'),
|
||||||
|
logList: $('log-list'),
|
||||||
|
logEmpty: $('log-empty'),
|
||||||
|
logCount: $('log-count'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOURCE_LABEL = {
|
||||||
|
local: 'Lokal',
|
||||||
|
oauth: 'Server',
|
||||||
|
app: 'App',
|
||||||
|
};
|
||||||
|
|
||||||
|
function fmtTime(ts) {
|
||||||
|
const d = new Date(ts);
|
||||||
|
const now = new Date();
|
||||||
|
const time = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
if (d.toDateString() === now.toDateString()) return time;
|
||||||
|
return `${d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} ${time}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatusRow(node, severity, label) {
|
||||||
|
node.dataset.severity = severity;
|
||||||
|
node.querySelector('.status-label').textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(s) {
|
||||||
|
if (!s) return;
|
||||||
|
|
||||||
|
const ep = s.endpoints || { local: { ok: true }, oauth: { ok: true, text: '—' } };
|
||||||
|
renderStatusRow(el.statusLocal, ep.local.ok === false ? 'critical' : 'ok', ep.local.ok === false ? 'gestört' : 'aktiv');
|
||||||
|
renderStatusRow(el.statusOauth, ep.oauth.ok === false ? 'warn' : 'ok', ep.oauth.text || '—');
|
||||||
|
|
||||||
|
const entries = (s.errors || []).slice().reverse();
|
||||||
|
el.logCount.textContent = entries.length ? `${entries.length} Einträge` : '';
|
||||||
|
el.logEmpty.hidden = entries.length > 0;
|
||||||
|
el.logList.replaceChildren(
|
||||||
|
...entries.map((e) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'log-entry';
|
||||||
|
|
||||||
|
const time = document.createElement('span');
|
||||||
|
time.className = 'log-time';
|
||||||
|
time.textContent = fmtTime(e.ts);
|
||||||
|
|
||||||
|
const source = document.createElement('span');
|
||||||
|
source.className = 'log-source';
|
||||||
|
source.dataset.source = e.source;
|
||||||
|
source.textContent = SOURCE_LABEL[e.source] || e.source;
|
||||||
|
|
||||||
|
const message = document.createElement('span');
|
||||||
|
message.className = 'log-message';
|
||||||
|
message.textContent = e.message;
|
||||||
|
|
||||||
|
li.append(time, source, message);
|
||||||
|
return li;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.close.addEventListener('click', () => window.errorsBridge.close());
|
||||||
|
el.clear.addEventListener('click', () => window.errorsBridge.clear());
|
||||||
|
document.addEventListener('keydown', (ev) => {
|
||||||
|
if (ev.key === 'Escape') window.errorsBridge.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.errorsBridge.onState(render);
|
||||||
|
window.errorsBridge.getState().then(render);
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<!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>Endpunkt-Status</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="titlebar">
|
||||||
|
<span class="title">Endpunkt-Status</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button class="icon-btn" id="btn-close" title="Schließen" aria-label="Schließen">✕</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="content">
|
||||||
|
<section class="group">
|
||||||
|
<h2>Aktueller Status</h2>
|
||||||
|
<dl class="facts">
|
||||||
|
<dt>Lokale Transkripte</dt>
|
||||||
|
<dd id="status-local"><span class="status-dot" aria-hidden="true"></span><span class="status-label">—</span></dd>
|
||||||
|
<dt>Serverabfrage</dt>
|
||||||
|
<dd id="status-oauth"><span class="status-dot" aria-hidden="true"></span><span class="status-label">—</span></dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<h2>Verlauf</h2>
|
||||||
|
<ul class="log" id="log-list"></ul>
|
||||||
|
<p class="empty" id="log-empty" hidden>Keine Meldungen bisher.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="actions">
|
||||||
|
<button class="btn ghost" id="btn-clear">Log löschen</button>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<span class="count" id="log-count"></span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
/* Fehler-/Ereignis-Log-Fenster — gleiche Farbwelt wie das Widget, aber auf
|
||||||
|
Lesbarkeit in einem größeren Fenster ausgelegt. */
|
||||||
|
|
||||||
|
: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;
|
||||||
|
--good: #0ca30c;
|
||||||
|
--warning: #fab219;
|
||||||
|
--critical: #d03b3b;
|
||||||
|
|
||||||
|
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 14px 16px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group + .group {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 18px;
|
||||||
|
border-top: 1px solid var(--hairline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group h2 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Aktueller Status ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.facts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 6px 12px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts dt {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts dd {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status trägt immer Punkt + Text, nie Farbe allein. */
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--good);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd[data-severity="warn"] .status-dot {
|
||||||
|
background: var(--warning);
|
||||||
|
border-radius: 2px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
dd[data-severity="critical"] .status-dot {
|
||||||
|
background: var(--critical);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd[data-severity="warn"] .status-label {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
dd[data-severity="critical"] .status-label {
|
||||||
|
color: var(--critical);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Verlauf ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.log {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 1fr;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--hairline);
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-source {
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-source[data-source="oauth"] {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-source[data-source="local"] {
|
||||||
|
color: var(--critical);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-source[data-source="app"] {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Fußzeile ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 11px 16px;
|
||||||
|
border-top: 1px solid var(--hairline);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 1px solid var(--hairline);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
+67
-13
@@ -22,6 +22,8 @@ const DEBOUNCE_MS = 300;
|
|||||||
const SAFETY_POLL_MS = 15000;
|
const SAFETY_POLL_MS = 15000;
|
||||||
/** Taktung der Neuberechnung, damit Countdown und Burn-Rate nicht einfrieren. */
|
/** Taktung der Neuberechnung, damit Countdown und Burn-Rate nicht einfrieren. */
|
||||||
const RECOMPUTE_MS = 5000;
|
const RECOMPUTE_MS = 5000;
|
||||||
|
/** Obergrenze des Ereignis-/Fehlerprotokolls — ein Log braucht mehr Tiefe als ein Badge. */
|
||||||
|
const MAX_ERRORS = 50;
|
||||||
|
|
||||||
/** Übersetzt den Schweregrad des Servers in die eigenen Stufen. */
|
/** Übersetzt den Schweregrad des Servers in die eigenen Stufen. */
|
||||||
function serverSeverity(value) {
|
function serverSeverity(value) {
|
||||||
@@ -41,9 +43,10 @@ function serverSeverity(value) {
|
|||||||
|
|
||||||
class Collector extends EventEmitter {
|
class Collector extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @param {{config: Object, store: import('./store').Store, configError?: string|null}} deps
|
* @param {{config: Object, store: import('./store').Store, configError?: string|null,
|
||||||
|
* versionEvent?: {type: 'install'|'update', previousVersion: string|null, currentVersion: string}|null}} deps
|
||||||
*/
|
*/
|
||||||
constructor({ config, store, configError = null }) {
|
constructor({ config, store, configError = null, versionEvent = null }) {
|
||||||
super();
|
super();
|
||||||
this.config = config || {};
|
this.config = config || {};
|
||||||
this.store = store;
|
this.store = store;
|
||||||
@@ -64,8 +67,19 @@ class Collector extends EventEmitter {
|
|||||||
this.subscription = { subscriptionType: null, rateLimitTier: null };
|
this.subscription = { subscriptionType: null, rateLimitTier: null };
|
||||||
this.limits = null;
|
this.limits = null;
|
||||||
this.state = null;
|
this.state = null;
|
||||||
this.errors = [];
|
// Zeitgestempeltes Ereignis-/Fehlerprotokoll — überdauert Neustarts über
|
||||||
if (configError) this.errors.push(configError);
|
// store.data.errors, siehe _note()/clearErrors().
|
||||||
|
this.errors = Array.isArray(store.data.errors) ? store.data.errors : [];
|
||||||
|
this._localOk = true;
|
||||||
|
this._lastOauthError = null;
|
||||||
|
if (configError) this._note(configError, 'local');
|
||||||
|
if (versionEvent) {
|
||||||
|
const msg =
|
||||||
|
versionEvent.type === 'update'
|
||||||
|
? `Aktualisiert: ${versionEvent.previousVersion} → ${versionEvent.currentVersion}`
|
||||||
|
: `Erstinstallation: Version ${versionEvent.currentVersion}`;
|
||||||
|
this._note(msg, 'app');
|
||||||
|
}
|
||||||
|
|
||||||
this._watcher = null;
|
this._watcher = null;
|
||||||
this._debounce = null;
|
this._debounce = null;
|
||||||
@@ -114,9 +128,10 @@ class Collector extends EventEmitter {
|
|||||||
this.oauth = new OAuthUsage(this.config.oauth || {}, this.store);
|
this.oauth = new OAuthUsage(this.config.oauth || {}, this.store);
|
||||||
|
|
||||||
this.limits = new LimitModel(this.config, this.store.data.calibration, this.subscription);
|
this.limits = new LimitModel(this.config, this.store.data.calibration, this.subscription);
|
||||||
this.errors = [];
|
// this.errors bleibt bewusst erhalten — das Ereignisprotokoll soll
|
||||||
|
// Einstellungs-Änderungen überstehen, nicht bei jedem Speichern verfallen.
|
||||||
|
|
||||||
this._recompute().catch((err) => this._note(`Neuberechnung fehlgeschlagen: ${err.message}`));
|
this._recompute().catch((err) => this._note(`Neuberechnung fehlgeschlagen: ${err.message}`, 'local'));
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
@@ -140,19 +155,33 @@ class Collector extends EventEmitter {
|
|||||||
this._debounce = setTimeout(() => this._poll(), DEBOUNCE_MS);
|
this._debounce = setTimeout(() => this._poll(), DEBOUNCE_MS);
|
||||||
});
|
});
|
||||||
this._watcher.on('error', (err) => {
|
this._watcher.on('error', (err) => {
|
||||||
this._note(`Dateiüberwachung gestört: ${err.message} — es wird weiter regelmäßig abgefragt.`);
|
this._note(`Dateiüberwachung gestört: ${err.message} — es wird weiter regelmäßig abgefragt.`, 'local');
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ohne Watcher greift das Sicherheitsnetz alle 15 s.
|
// Ohne Watcher greift das Sicherheitsnetz alle 15 s.
|
||||||
this._note(`Dateiüberwachung nicht verfügbar: ${err.message}`);
|
this._note(`Dateiüberwachung nicht verfügbar: ${err.message}`, 'local');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_note(message) {
|
/**
|
||||||
if (!this.errors.includes(message)) {
|
* Hält das Ereignis-/Fehlerprotokoll fest — zeitgestempelt, dedupliziert
|
||||||
this.errors.push(message);
|
* nach Meldungstext und in `store.data.errors` persistiert, damit es
|
||||||
if (this.errors.length > 5) this.errors.shift();
|
* Neustarts und Einstellungs-Änderungen übersteht.
|
||||||
|
*/
|
||||||
|
_note(message, source = 'local') {
|
||||||
|
if (this.errors.some((e) => e.message === message)) return;
|
||||||
|
this.errors.push({ ts: Date.now(), message, source });
|
||||||
|
if (this.errors.length > MAX_ERRORS) this.errors.shift();
|
||||||
|
this.store.data.errors = this.errors;
|
||||||
|
this.store.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Leert das Ereignisprotokoll — ausgelöst über den „Log löschen"-Knopf im Fehler-Log-Fenster. */
|
||||||
|
clearErrors() {
|
||||||
|
this.errors = [];
|
||||||
|
this.store.data.errors = this.errors;
|
||||||
|
this.store.save();
|
||||||
|
this._recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _poll() {
|
async _poll() {
|
||||||
@@ -167,9 +196,14 @@ class Collector extends EventEmitter {
|
|||||||
if (entries.length) this.aggregator.add(entries);
|
if (entries.length) this.aggregator.add(entries);
|
||||||
this.aggregator.prune();
|
this.aggregator.prune();
|
||||||
this._lastPollMs = Date.now() - t0;
|
this._lastPollMs = Date.now() - t0;
|
||||||
|
if (!this._localOk) {
|
||||||
|
this._localOk = true;
|
||||||
|
this._note('Dateizugriff wieder erreichbar', 'local');
|
||||||
|
}
|
||||||
await this._recompute();
|
await this._recompute();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._note(`Lesen fehlgeschlagen: ${err.message}`);
|
this._localOk = false;
|
||||||
|
this._note(`Lesen fehlgeschlagen: ${err.message}`, 'local');
|
||||||
} finally {
|
} finally {
|
||||||
this._polling = false;
|
this._polling = false;
|
||||||
if (this._pollAgain) {
|
if (this._pollAgain) {
|
||||||
@@ -207,6 +241,17 @@ class Collector extends EventEmitter {
|
|||||||
// Damit stimmt das lokale Fenster ohne jede Konfiguration mit dem echten
|
// Damit stimmt das lokale Fenster ohne jede Konfiguration mit dem echten
|
||||||
// überein — Voraussetzung dafür, dass die Live-Kalibrierung unten trägt.
|
// überein — Voraussetzung dafür, dass die Live-Kalibrierung unten trägt.
|
||||||
const server = await this.oauth.fetch(now);
|
const server = await this.oauth.fetch(now);
|
||||||
|
|
||||||
|
// Übergänge des Server-Fehlers ins Ereignisprotokoll übernehmen — nicht
|
||||||
|
// jeden Poll, nur den Wechsel. "abgeschaltet"/"nicht konfiguriert"/
|
||||||
|
// "wartet" sind kein Fehler und lösen bewusst keinen Eintrag aus.
|
||||||
|
const oauthError = this.oauth.lastError;
|
||||||
|
if (oauthError !== this._lastOauthError) {
|
||||||
|
if (oauthError) this._note(`Serverabfrage: ${oauthError}`, 'oauth');
|
||||||
|
else if (this._lastOauthError) this._note('Serverabfrage wieder erreichbar', 'oauth');
|
||||||
|
this._lastOauthError = oauthError;
|
||||||
|
}
|
||||||
|
|
||||||
const anchor = server?.weekResetAt
|
const anchor = server?.weekResetAt
|
||||||
? server.weekResetAt - this.aggregator.weekMs
|
? server.weekResetAt - this.aggregator.weekMs
|
||||||
: weekAnchor(now, this.config);
|
: weekAnchor(now, this.config);
|
||||||
@@ -327,6 +372,15 @@ class Collector extends EventEmitter {
|
|||||||
extras: server?.extras || [],
|
extras: server?.extras || [],
|
||||||
history: snapshot.blockHistory,
|
history: snapshot.blockHistory,
|
||||||
oauth: this.oauth.status(),
|
oauth: this.oauth.status(),
|
||||||
|
// Für das Endpunkt-Status-Icon im Titelbalken: lokale Transkripte sind
|
||||||
|
// die Kerndatenquelle (ohne sie funktioniert das Dashboard gar nicht)
|
||||||
|
// → critical; der Server-Endpunkt ist laut Doku in oauth-usage.js
|
||||||
|
// ausdrücklich optional/best-effort → nur warn.
|
||||||
|
endpoints: {
|
||||||
|
severity: !this._localOk ? 'critical' : oauthError ? 'warn' : 'ok',
|
||||||
|
local: { ok: this._localOk },
|
||||||
|
oauth: { ok: !oauthError, text: this.oauth.status().text },
|
||||||
|
},
|
||||||
errors: this.errors.slice(),
|
errors: this.errors.slice(),
|
||||||
stats: { lastPollMs: this._lastPollMs },
|
stats: { lastPollMs: this._lastPollMs },
|
||||||
};
|
};
|
||||||
|
|||||||
+85
-2
@@ -7,7 +7,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 } = require('electron');
|
const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeImage, shell, Notification } = require('electron');
|
||||||
|
|
||||||
const { Store, loadConfig } = require('./store');
|
const { Store, loadConfig } = require('./store');
|
||||||
const { Collector } = require('./collector');
|
const { Collector } = require('./collector');
|
||||||
@@ -26,8 +26,12 @@ const ROOT = path.join(__dirname, '..', '..');
|
|||||||
const SETTINGS_WIDTH = 520;
|
const SETTINGS_WIDTH = 520;
|
||||||
const SETTINGS_HEIGHT = 660;
|
const SETTINGS_HEIGHT = 660;
|
||||||
|
|
||||||
|
const ERRORS_WIDTH = 420;
|
||||||
|
const ERRORS_HEIGHT = 480;
|
||||||
|
|
||||||
let win = null;
|
let win = null;
|
||||||
let settingsWin = null;
|
let settingsWin = null;
|
||||||
|
let errorsWin = null;
|
||||||
let tray = null;
|
let tray = null;
|
||||||
let collector = null;
|
let collector = null;
|
||||||
let store = null;
|
let store = null;
|
||||||
@@ -301,6 +305,50 @@ function openSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fehler-/Ereignis-Log — ein Fenster, mehrfaches Öffnen holt es nur nach vorn. */
|
||||||
|
function openErrorLog() {
|
||||||
|
if (errorsWin && !errorsWin.isDestroyed()) {
|
||||||
|
errorsWin.show();
|
||||||
|
errorsWin.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = win && !win.isDestroyed()
|
||||||
|
? screen.getDisplayMatching(win.getBounds())
|
||||||
|
: screen.getPrimaryDisplay();
|
||||||
|
const area = target.workArea;
|
||||||
|
|
||||||
|
errorsWin = new BrowserWindow({
|
||||||
|
width: ERRORS_WIDTH,
|
||||||
|
height: ERRORS_HEIGHT,
|
||||||
|
x: Math.round(area.x + (area.width - ERRORS_WIDTH) / 2),
|
||||||
|
y: Math.round(area.y + Math.max(0, (area.height - ERRORS_HEIGHT) / 2)),
|
||||||
|
minWidth: 360,
|
||||||
|
minHeight: 320,
|
||||||
|
frame: false,
|
||||||
|
resizable: true,
|
||||||
|
skipTaskbar: false,
|
||||||
|
// Über dem Widget, das selbst always-on-top ist — sonst verschwindet der
|
||||||
|
// Dialog dahinter.
|
||||||
|
alwaysOnTop: true,
|
||||||
|
show: false,
|
||||||
|
backgroundColor: '#1e1e22',
|
||||||
|
icon: path.join(ROOT, 'assets', 'icon.png'),
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, '..', 'errors-preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
errorsWin.loadFile(path.join(__dirname, '..', 'errors', 'index.html'));
|
||||||
|
errorsWin.once('ready-to-show', () => errorsWin.show());
|
||||||
|
errorsWin.on('closed', () => {
|
||||||
|
errorsWin = 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
|
||||||
@@ -345,6 +393,9 @@ function registerIpc() {
|
|||||||
ipcMain.on('set-expanded', (_event, expanded) => applyExpanded(Boolean(expanded)));
|
ipcMain.on('set-expanded', (_event, expanded) => applyExpanded(Boolean(expanded)));
|
||||||
ipcMain.on('hide-window', () => win && win.hide());
|
ipcMain.on('hide-window', () => win && win.hide());
|
||||||
ipcMain.on('open-settings', openSettings);
|
ipcMain.on('open-settings', openSettings);
|
||||||
|
ipcMain.on('open-error-log', openErrorLog);
|
||||||
|
ipcMain.on('errors:clear', () => collector && collector.clearErrors());
|
||||||
|
ipcMain.on('errors:close', () => errorsWin && errorsWin.close());
|
||||||
|
|
||||||
ipcMain.handle('settings:load', () => ({
|
ipcMain.handle('settings:load', () => ({
|
||||||
config,
|
config,
|
||||||
@@ -352,6 +403,7 @@ function registerIpc() {
|
|||||||
configPath: CONFIG_FILE,
|
configPath: CONFIG_FILE,
|
||||||
autostart: getAutostart(),
|
autostart: getAutostart(),
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
version: app.getVersion(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -386,6 +438,23 @@ async function main() {
|
|||||||
store = new Store(STATE_FILE);
|
store = new Store(STATE_FILE);
|
||||||
store.load();
|
store.load();
|
||||||
|
|
||||||
|
// Neuinstallations-/Update-Erkennung: rein additiv, überschreibt oder
|
||||||
|
// löscht keine bestehenden Einstellungsdaten — nur `store.data.appVersion`
|
||||||
|
// wird gesetzt. Funktioniert identisch unter Windows (NSIS/portable) und
|
||||||
|
// Linux (AppImage/deb), da app.getVersion() plattformunabhängig aus den
|
||||||
|
// Paketmetadaten liest und state.json auf beiden Plattformen im selben
|
||||||
|
// Nutzerprofil-Ordner liegt (siehe paths.js).
|
||||||
|
const currentVersion = app.getVersion();
|
||||||
|
const previousVersion = store.data.appVersion;
|
||||||
|
let versionEvent = null;
|
||||||
|
if (previousVersion == null) {
|
||||||
|
versionEvent = { type: 'install', previousVersion: null, currentVersion };
|
||||||
|
} else if (previousVersion !== currentVersion) {
|
||||||
|
versionEvent = { type: 'update', previousVersion, currentVersion };
|
||||||
|
}
|
||||||
|
store.data.appVersion = currentVersion;
|
||||||
|
store.save();
|
||||||
|
|
||||||
registerIpc();
|
registerIpc();
|
||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
@@ -393,9 +462,23 @@ async function main() {
|
|||||||
if (config.opacity != null) win.setOpacity(config.opacity);
|
if (config.opacity != null) win.setOpacity(config.opacity);
|
||||||
if (config.clickThrough) win.setIgnoreMouseEvents(true, { forward: true });
|
if (config.clickThrough) win.setIgnoreMouseEvents(true, { forward: true });
|
||||||
|
|
||||||
collector = new Collector({ config, store, configError: error });
|
collector = new Collector({ config, store, configError: error, versionEvent });
|
||||||
|
|
||||||
|
// Hinweis auf ein Update über die native Systembenachrichtigung — der
|
||||||
|
// Titelbalken ist mit dem Endpunkt-Status-Icon bereits eng, und eine
|
||||||
|
// Notification ist naturgemäß einmalig. Eine Neuinstallation bekommt
|
||||||
|
// bewusst keine Meldung: dafür gibt es nichts, worüber zu informieren wäre.
|
||||||
|
if (versionEvent?.type === 'update' && Notification.isSupported()) {
|
||||||
|
new Notification({
|
||||||
|
title: 'Claude Live Dashboard aktualisiert',
|
||||||
|
body: `Version ${versionEvent.previousVersion} → ${versionEvent.currentVersion}`,
|
||||||
|
icon: path.join(ROOT, 'assets', 'icon.png'),
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
|
||||||
collector.on('update', (state) => {
|
collector.on('update', (state) => {
|
||||||
if (win && !win.isDestroyed()) win.webContents.send('state', state);
|
if (win && !win.isDestroyed()) win.webContents.send('state', state);
|
||||||
|
if (errorsWin && !errorsWin.isDestroyed()) errorsWin.webContents.send('state', state);
|
||||||
if (tray) {
|
if (tray) {
|
||||||
const parts = [`Claude · Woche ${Math.round(state.week.percent)} %`];
|
const parts = [`Claude · Woche ${Math.round(state.week.percent)} %`];
|
||||||
if (state.block.hasLimit) parts.push(`5h ${Math.round(state.block.percent)} %`);
|
if (state.block.hasLimit) parts.push(`5h ${Math.round(state.block.percent)} %`);
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ const DEFAULT_STATE = {
|
|||||||
calibration: { blockMax: 0, weekMax: 0, samples: 0, serverLimits: {} },
|
calibration: { blockMax: 0, weekMax: 0, samples: 0, serverLimits: {} },
|
||||||
window: { x: null, y: null, expanded: false },
|
window: { x: null, y: null, expanded: false },
|
||||||
oauth: { lastFetch: 0, pausedUntil: 0 },
|
oauth: { lastFetch: 0, pausedUntil: 0 },
|
||||||
|
// Zeitgestempeltes Ereignis-/Fehlerprotokoll (lokale Transkripte,
|
||||||
|
// Serverabfrage, App-Lebenszyklus) — überdauert bewusst Neustarts.
|
||||||
|
errors: [],
|
||||||
|
// Zuletzt gestartete App-Version, für die Neuinstallations-/Update-Erkennung.
|
||||||
|
appVersion: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
@@ -46,6 +51,7 @@ class Store {
|
|||||||
calibration: { ...DEFAULT_STATE.calibration, ...(parsed.calibration || {}) },
|
calibration: { ...DEFAULT_STATE.calibration, ...(parsed.calibration || {}) },
|
||||||
window: { ...DEFAULT_STATE.window, ...(parsed.window || {}) },
|
window: { ...DEFAULT_STATE.window, ...(parsed.window || {}) },
|
||||||
oauth: { ...DEFAULT_STATE.oauth, ...(parsed.oauth || {}) },
|
oauth: { ...DEFAULT_STATE.oauth, ...(parsed.oauth || {}) },
|
||||||
|
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code !== 'ENOENT') {
|
if (err.code !== 'ENOENT') {
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ contextBridge.exposeInMainWorld('dashboard', {
|
|||||||
hide: () => ipcRenderer.send('hide-window'),
|
hide: () => ipcRenderer.send('hide-window'),
|
||||||
/** Öffnet den Einstellungsdialog. */
|
/** Öffnet den Einstellungsdialog. */
|
||||||
openSettings: () => ipcRenderer.send('open-settings'),
|
openSettings: () => ipcRenderer.send('open-settings'),
|
||||||
|
/** Öffnet das Fehler-/Ereignis-Log. */
|
||||||
|
openErrorLog: () => ipcRenderer.send('open-error-log'),
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-7
@@ -46,7 +46,7 @@ const el = {
|
|||||||
widget: $('widget'),
|
widget: $('widget'),
|
||||||
planLabel: $('plan-label'),
|
planLabel: $('plan-label'),
|
||||||
staleBadge: $('stale-badge'),
|
staleBadge: $('stale-badge'),
|
||||||
errorBadge: $('error-badge'),
|
btnEndpointStatus: $('btn-endpoint-status'),
|
||||||
btnHide: $('btn-hide'),
|
btnHide: $('btn-hide'),
|
||||||
btnSettings: $('btn-settings'),
|
btnSettings: $('btn-settings'),
|
||||||
btnExpand: $('btn-expand'),
|
btnExpand: $('btn-expand'),
|
||||||
@@ -275,6 +275,15 @@ function renderSpend(spend) {
|
|||||||
el.spendA11y.textContent = `Nutzungsguthaben: ${money.format(spend.used)} von ${money.format(spend.limit)} verbraucht.`;
|
el.spendA11y.textContent = `Nutzungsguthaben: ${money.format(spend.used)} von ${money.format(spend.limit)} verbraucht.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Endpunkt-Status-Icon im Titelbalken — Zustand der beiden Datenquellen. */
|
||||||
|
function renderEndpointStatus(s) {
|
||||||
|
const ep = s.endpoints || { severity: 'ok', local: { ok: true }, oauth: { ok: true, text: '—' } };
|
||||||
|
el.btnEndpointStatus.dataset.severity = ep.severity;
|
||||||
|
el.btnEndpointStatus.title =
|
||||||
|
`Lokale Transkripte: ${ep.local.ok === false ? 'gestört' : 'aktiv'} · ` +
|
||||||
|
`Serverabfrage: ${ep.oauth.text || '—'}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderWeek(week, burn) {
|
function renderWeek(week, burn) {
|
||||||
renderMeter('week', week);
|
renderMeter('week', week);
|
||||||
renderProjection(el.weekProjection, week.percent, burn.weekProjectedPercent, week.severity);
|
renderProjection(el.weekProjection, week.percent, burn.weekProjectedPercent, week.severity);
|
||||||
@@ -368,9 +377,9 @@ function renderDetails(s) {
|
|||||||
if (s.errors && s.errors.length) {
|
if (s.errors && s.errors.length) {
|
||||||
el.errors.hidden = false;
|
el.errors.hidden = false;
|
||||||
el.errors.replaceChildren(
|
el.errors.replaceChildren(
|
||||||
...s.errors.map((msg) => {
|
...s.errors.slice(-5).reverse().map((e) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.textContent = msg;
|
li.textContent = e.message;
|
||||||
return li;
|
return li;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -387,10 +396,7 @@ function render(s) {
|
|||||||
renderBlock(s.block, s.burn);
|
renderBlock(s.block, s.burn);
|
||||||
renderWeek(s.week, s.burn);
|
renderWeek(s.week, s.burn);
|
||||||
renderSpend(s.spend);
|
renderSpend(s.spend);
|
||||||
|
renderEndpointStatus(s);
|
||||||
// Meldungen stehen in der Detailansicht — in der Kopfzeile muss aber
|
|
||||||
// sichtbar sein, dass es überhaupt eine gibt.
|
|
||||||
el.errorBadge.hidden = !(s.errors && s.errors.length);
|
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
renderModels(s.block);
|
renderModels(s.block);
|
||||||
@@ -455,6 +461,7 @@ el.btnExpand.addEventListener('click', () => applyExpanded(!expanded));
|
|||||||
|
|
||||||
el.btnHide.addEventListener('click', () => window.dashboard.hide());
|
el.btnHide.addEventListener('click', () => window.dashboard.hide());
|
||||||
el.btnSettings.addEventListener('click', () => window.dashboard.openSettings());
|
el.btnSettings.addEventListener('click', () => window.dashboard.openSettings());
|
||||||
|
el.btnEndpointStatus.addEventListener('click', () => window.dashboard.openErrorLog());
|
||||||
|
|
||||||
window.dashboard.onState(render);
|
window.dashboard.onState(render);
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
<span class="dot-brand" aria-hidden="true"></span>
|
<span class="dot-brand" aria-hidden="true"></span>
|
||||||
<span class="title" id="plan-label">Claude</span>
|
<span class="title" id="plan-label">Claude</span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<span class="badge warn" id="error-badge" hidden title="Details öffnen für die Meldung">Hinweis</span>
|
|
||||||
<span class="badge warn" id="stale-badge" hidden>veraltet</span>
|
<span class="badge warn" id="stale-badge" hidden>veraltet</span>
|
||||||
|
<button class="icon-btn status" id="btn-endpoint-status" data-severity="ok" title="Endpunkt-Status" aria-label="Endpunkt-Status">
|
||||||
|
<span class="status-icon" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
<button class="icon-btn" id="btn-settings" title="Einstellungen" aria-label="Einstellungen">
|
<button class="icon-btn" id="btn-settings" title="Einstellungen" aria-label="Einstellungen">
|
||||||
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" focusable="false">
|
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" focusable="false">
|
||||||
<path
|
<path
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ function setNum(input, value, fallback = '') {
|
|||||||
|
|
||||||
/** Füllt die Oberfläche aus der Konfiguration. */
|
/** Füllt die Oberfläche aus der Konfiguration. */
|
||||||
function fill(config, meta) {
|
function fill(config, meta) {
|
||||||
el.path.textContent = meta.configPath;
|
el.path.textContent = meta.version ? `${meta.configPath} · Version ${meta.version}` : meta.configPath;
|
||||||
|
|
||||||
const opacity = Math.round((config.opacity ?? DEFAULTS.opacity) * 100);
|
const opacity = Math.round((config.opacity ?? DEFAULTS.opacity) * 100);
|
||||||
el.opacity.value = String(opacity);
|
el.opacity.value = String(opacity);
|
||||||
|
|||||||
Reference in New Issue
Block a user