Linux-Fassung (Ubuntu) ergaenzen

Die Fachlogik war bereits plattformneutral — paths.js loeste ~/.config
schon auf, jsonl.js liest ueber os.homedir(). Zu tun waren Autostart,
Build-Targets und drei Linux-Eigenheiten der Oberflaeche.

XWayland erzwingen: Ab Electron 36 waehlt Chromium in einer
Wayland-Sitzung von sich aus Wayland. Dort darf sich ein Fenster weder
selbst positionieren noch dauerhaft in den Vordergrund legen — beides
ist der Kern dieses Widgets. Der Schalter muss auf der Kommandozeile
stehen, weil Ozone die Plattform vor dem Main-Skript festlegt;
appendSwitch() und ELECTRON_OZONE_PLATFORM_HINT kommen beide zu spaet.
Bleibt ein einmaliger Neustart mit --ozone-platform=x11, der sich am
Argument selbst als Abbruchbedingung erkennt. Im AppImage muss dabei
execPath auf APPIMAGE zeigen, sonst laeuft der Nachfolger in den
/tmp-Einhaengepunkt, den der Vorgaenger beim Beenden wegraeumt.

Autostart: app.setLoginItemSettings() gibt es unter Linux nicht. Das
neue Modul autostart.js kapselt die Weiche und schreibt bzw. loescht
dort ~/.config/autostart/claude-live-dashboard.desktop. Der Startbefehl
kommt aus APPIMAGE, falls gesetzt.

Ausserdem: enable-transparent-visuals fuer echte Transparenz, das
32-px-Tray-Symbol in der GNOME-Leiste, plattformabhaengige
Beschriftungen in Tray-Menue und Einstellungsdialog, AppImage- und
deb-Targets, icon.png auf 512 px (electron-builder rechnet daraus die
Linux-Groessen herunter) und start.sh als Gegenstueck zu start.cmd.

Getestet unter GNOME/Wayland: Fenster auf X11 an erwarteter Position,
gemerkte Position wird wiederhergestellt, Tray registriert sich auf
DBus, dump liest die Transkripte, beide Pakete bauen und das AppImage
startet ohne Schalter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 21:52:08 +02:00
co-authored by Claude Opus 4.8
parent 868f31fa9a
commit c1b899ec8b
9 changed files with 294 additions and 23 deletions
+91 -5
View File
@@ -1,6 +1,6 @@
# Claude Live Dashboard
Ein rahmenloses Always-on-Top-Widget für Windows, das die Auslastung des
Ein rahmenloses Always-on-Top-Widget für Windows und Linux, das die Auslastung des
Claude-Plans live anzeigt: 5-Stunden-Fenster, Wochenverbrauch, Reset-Countdown,
Tokenverbrauch je Modell und die Hochrechnung auf das Fensterende.
@@ -10,8 +10,12 @@ mitgeschnitten und standardmäßig keine Netzwerkverbindung aufgebaut.
## Start
Node.js wird benötigt. Ist es nicht im `PATH` (etwa bei nvm-windows ohne
aktive Version), genügt der Versionspfad:
Node.js (ab Version 20) wird benötigt.
### Windows
Ist Node nicht im `PATH` (etwa bei nvm-windows ohne aktive Version), genügt der
Versionspfad:
```powershell
$env:PATH = "$env:APPDATA\nvm\v22.22.1;$env:PATH"
@@ -19,9 +23,51 @@ npm install
npm start
```
Alternativ `start.cmd` doppelklicken.
Das Tray-Symbol landet unter Windows zunächst im Überlaufbereich hinter dem
`^` in der Taskleiste; per Drag lässt es sich dauerhaft sichtbar machen.
### Linux (Ubuntu)
```bash
sudo apt install nodejs npm # falls noch nicht vorhanden
./start.sh
```
`start.sh` installiert fehlende Abhängigkeiten selbst und startet Electron;
`npm install && npm start` tut dasselbe.
Das Tray-Symbol braucht unter GNOME die Erweiterung **AppIndicator** — beim
Ubuntu-Desktop ist sie vorinstalliert und aktiv. Auf einer Minimalinstallation
oder unter anderen Oberflächen:
```bash
sudo apt install gnome-shell-extension-appindicator
```
Ohne sie läuft das Widget weiter, nur das Symbol in der oberen Leiste fehlt.
Anders als unter Windows ist im StatusNotifierItem-Standard nicht festgelegt,
welcher Klick als Aktivierung zählt — verlässlich ist deshalb das Kontextmenü
(Rechtsklick) mit „Dashboard anzeigen“ als erstem Eintrag.
**Wayland:** Ab Electron 36 wählt Chromium in einer Wayland-Sitzung von sich
aus Wayland — dort darf sich ein Fenster aber weder selbst positionieren noch
dauerhaft in den Vordergrund legen, und genau das ist der Kern dieses Widgets.
Die App startet sich deshalb unter Wayland einmalig selbst mit
`--ozone-platform=x11` neu und läuft über XWayland. Das geschieht automatisch,
es ist nichts einzustellen.
Der Schalter *muss* auf der Kommandozeile stehen: Ozone legt die Plattform
fest, bevor das Main-Skript läuft — `app.commandLine.appendSwitch()` und
`ELECTRON_OZONE_PLATFORM_HINT` kommen beide zu spät. Wer die Automatik
umgehen will, setzt eine der beiden Größen von Hand; dann greift der Neustart
nicht mehr:
```bash
ELECTRON_OZONE_PLATFORM_HINT=wayland ./start.sh # nativ Wayland, ohne Positionierung
```
## Bedienung
| Aktion | Wirkung |
@@ -157,6 +203,7 @@ src/main/oauth-usage.js Serverabfrage, gekapselt und ausfallsicher
src/main/calibrate.js Kalibrierung gegen die echte Anzeige
src/main/store.js state.json, config.json
src/main/paths.js Speicherorte für Konfiguration und Zustand
src/main/autostart.js Anmeldestart je Betriebssystem
src/preload.js contextBridge fürs Widget
src/settings-preload.js contextBridge für den Einstellungsdialog
src/renderer/ Widget-Oberfläche
@@ -178,6 +225,12 @@ Zwei Details, die nicht offensichtlich sind:
## Release bauen
Gebaut wird jeweils auf dem Zielsystem — electron-builder kann `.deb` nicht
sinnvoll von Windows aus erzeugen und umgekehrt keine signierten `.exe` unter
Linux.
### Windows
```powershell
npm run dist # Installer + portable Fassung nach release/
npm run pack # nur entpackt nach release/win-unpacked (schneller Test)
@@ -201,16 +254,49 @@ Start einen SmartScreen-Hinweis („Der Computer wurde durch Windows geschützt
Weitergabe an andere wäre ein Zertifikat nötig; für den Eigenbedarf ist es
verzichtbar.
### Linux
```bash
npm run dist:linux # AppImage + .deb nach release/
npm run pack:linux # nur entpackt nach release/linux-unpacked
```
Ergebnis in `release/`:
| Datei | Zweck |
|---|---|
| `Claude Live Dashboard-1.0.0-x64.AppImage` | läuft ohne Installation |
| `Claude Live Dashboard-1.0.0-amd64.deb` | Installation per `sudo apt install ./…deb` |
Das AppImage muss einmal ausführbar gemacht werden (`chmod +x`) und braucht
`libfuse2`. Es trägt sich nicht selbst ins Anwendungsmenü ein — der Autostart
über das Tray-Menü funktioniert trotzdem, weil die App den Pfad des Abbilds aus
`APPIMAGE` liest und nicht den temporären Einhängepunkt. Das `.deb` bringt
Menüeintrag und Symbol mit und installiert nach `/opt/Claude Live Dashboard`.
> **Ubuntu 24.04 und AppImage.** Seit 24.04 beschränkt AppArmor unprivilegierte
> User-Namespaces; die Chromium-Sandbox kann dann beim Start fehlschlagen
> (`SUID sandbox helper binary … is not configured correctly`). Zwei Auswege:
> das AppImage mit `--no-sandbox` starten, oder ein AppArmor-Profil unter
> `/etc/apparmor.d/` hinterlegen. Beim `.deb` tritt das nicht auf — dessen
> `postinst` setzt das SUID-Bit auf `chrome-sandbox`.
### Konfiguration und Zustand
Beides liegt im Benutzerprofil, nicht im Programmverzeichnis — dort wäre es
nach der Installation im asar-Archiv und schreibgeschützt:
```
%APPDATA%\Claude Live Dashboard\config.json
%APPDATA%\Claude Live Dashboard\state.json
Windows: %APPDATA%\Claude Live Dashboard\config.json
%APPDATA%\Claude Live Dashboard\state.json
Linux: ~/.config/Claude Live Dashboard/config.json
~/.config/Claude Live Dashboard/state.json
```
Der Autostart ist kein Feld dieser Dateien: unter Windows steckt er in der
Registry, unter Linux in `~/.config/autostart/claude-live-dashboard.desktop`.
Beim ersten Start wird `config.json` aus der mitgelieferten Vorlage angelegt.
Erreichbar über das Tray-Menü (*Konfiguration öffnen* / *Datenordner öffnen*).
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

+32 -2
View File
@@ -3,7 +3,7 @@
"productName": "Claude Live Dashboard",
"version": "1.0.0",
"private": true,
"description": "Always-on-Top Mini-Dashboard für Claude-Plan-Auslastung unter Windows",
"description": "Always-on-Top Mini-Dashboard für Claude-Plan-Auslastung unter Windows und Linux",
"main": "src/main/index.js",
"scripts": {
"start": "electron .",
@@ -11,10 +11,14 @@
"calibrate": "node src/main/calibrate.js",
"icons": "node tools/make-icon.js",
"dist": "electron-builder --win",
"pack": "electron-builder --win --dir"
"pack": "electron-builder --win --dir",
"dist:linux": "electron-builder --linux",
"pack:linux": "electron-builder --linux --dir"
},
"author": "",
"license": "MIT",
"_homepage": "Platzhalter — nur nötig, weil das .deb-Paket ein Homepage-Feld verlangt.",
"homepage": "https://github.com/exilkieler/claude-live-dashboard",
"devDependencies": {
"electron": "^43.2.0",
"electron-builder": "^25.1.8"
@@ -66,6 +70,32 @@
},
"portable": {
"artifactName": "${productName}-${version}-portable.${ext}"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
],
"icon": "assets/icon.png",
"executableName": "claude-live-dashboard",
"category": "Utility",
"maintainer": "Stefan <exilkieler@gmail.com>",
"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"
},
"artifactName": "${productName}-${version}-${arch}.${ext}"
}
}
}
+84
View File
@@ -0,0 +1,84 @@
'use strict';
/**
* Autostart — die einzige Stelle, die weiß, wie die jeweilige Arbeitsumgebung
* einen Anmeldestart hinterlegt.
*
* Unter Windows und macOS erledigt Electron das selbst. Unter Linux gibt es
* `app.setLoginItemSettings()` nicht; dort ist der Autostart eine
* `.desktop`-Datei in `~/.config/autostart`, deren bloße Existenz den Start
* auslöst.
*/
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { app } = require('electron');
const IS_LINUX = process.platform === 'linux';
const AUTOSTART_DIR = path.join(
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
'autostart',
);
const DESKTOP_FILE = path.join(AUTOSTART_DIR, 'claude-live-dashboard.desktop');
/**
* Der Befehl, mit dem sich die Anwendung selbst wieder startet.
*
* Ein AppImage entpackt sich beim Start in ein temporäres Verzeichnis —
* `process.execPath` zeigt dann dorthin und wäre beim nächsten Anmelden
* verschwunden. Den Pfad zum Abbild selbst liefert nur `APPIMAGE`.
*/
function launchCommand() {
if (process.env.APPIMAGE) return `"${process.env.APPIMAGE}"`;
if (!app.isPackaged) return `"${process.execPath}" "${app.getAppPath()}"`;
return `"${process.execPath}"`;
}
function desktopEntry() {
return [
'[Desktop Entry]',
'Type=Application',
'Version=1.0',
'Name=Claude Live Dashboard',
'Comment=Auslastung des Claude-Abos im Blick',
`Exec=${launchCommand()}`,
'Icon=claude-live-dashboard',
'Terminal=false',
'X-GNOME-Autostart-enabled=true',
'',
].join('\n');
}
/** @returns {boolean} true, wenn die Anwendung beim Anmelden startet */
function getAutostart() {
if (!IS_LINUX) return app.getLoginItemSettings().openAtLogin;
return fs.existsSync(DESKTOP_FILE);
}
/**
* Schaltet den Anmeldestart ein oder aus.
* @param {boolean} enabled
*/
function setAutostart(enabled) {
if (!IS_LINUX) {
app.setLoginItemSettings({ openAtLogin: enabled, args: [] });
return;
}
try {
if (enabled) {
fs.mkdirSync(AUTOSTART_DIR, { recursive: true });
fs.writeFileSync(DESKTOP_FILE, desktopEntry(), 'utf8');
} else {
fs.rmSync(DESKTOP_FILE, { force: true });
}
} catch (err) {
// Ein fehlgeschlagener Autostart ist kein Grund, die laufende Anwendung
// zu stören — der Haken im Menü zeigt beim nächsten Aufbau den echten Stand.
console.warn(`[autostart] konnte nicht gesetzt werden: ${err.message}`);
}
}
module.exports = { getAutostart, setAutostart, DESKTOP_FILE };
+62 -13
View File
@@ -12,6 +12,9 @@ const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeImage, shell } =
const { Store, loadConfig } = require('./store');
const { Collector } = require('./collector');
const { CONFIG_FILE, STATE_FILE, USER_DATA_DIR, ensureUserConfig } = require('./paths');
const { getAutostart, setAutostart: applyAutostart } = require('./autostart');
const IS_LINUX = process.platform === 'linux';
const COMPACT_HEIGHT = 352;
const EXPANDED_HEIGHT = 640;
@@ -30,11 +33,59 @@ let collector = null;
let store = null;
let config = {};
/**
* Erzwingt XWayland, sobald das Widget in einer Wayland-Sitzung startet.
*
* Electron wählt seit Version 36 von sich aus Wayland. Dort darf sich ein
* Fenster aber weder selbst positionieren noch dauerhaft in den Vordergrund
* legen — beides ist der Kern dieses Widgets. Über XWayland funktioniert es.
*
* Der Schalter muss auf der Kommandozeile stehen: Ozone wählt die Plattform,
* bevor dieses Skript läuft, deshalb kommen `appendSwitch()` und
* `ELECTRON_OZONE_PLATFORM_HINT` beide zu spät. Bleibt ein Neustart mit
* ergänztem Argument. Das Argument ist zugleich die Abbruchbedingung — im
* zweiten Durchlauf steht es in `argv` und der Zweig greift nicht mehr.
*
* Muss vor `requestSingleInstanceLock()` stehen, damit die verworfene Instanz
* die Sperre gar nicht erst nimmt.
*/
function forceX11IfWayland() {
if (!IS_LINUX || !process.env.WAYLAND_DISPLAY) return false;
if (process.env.ELECTRON_OZONE_PLATFORM_HINT) return false; // ausdrücklich übersteuert
if (process.argv.some((a) => a.startsWith('--ozone-platform'))) return false;
const args = process.argv.slice(1).concat('--ozone-platform=x11');
// Ein AppImage hängt sich beim Start unter /tmp ein und wirft die Einhängung
// beim Beenden wieder weg. `process.execPath` zeigt dorthin und wäre für den
// Nachfolger schon verschwunden — neu gestartet werden muss das Abbild.
if (process.env.APPIMAGE) app.relaunch({ execPath: process.env.APPIMAGE, args });
else app.relaunch({ args });
app.exit(0);
return true;
}
if (!forceX11IfWayland()) {
// Ohne diesen Schalter zeichnet Chromium unter Linux hinter das rahmenlose
// Fenster einen schwarzen Kasten statt echter Transparenz.
if (IS_LINUX) app.commandLine.appendSwitch('enable-transparent-visuals');
bootstrap();
}
/** Verhindert, dass mehrere Instanzen dieselben Zustandsdateien beschreiben. */
if (!app.requestSingleInstanceLock()) {
app.quit();
} else {
function bootstrap() {
if (!app.requestSingleInstanceLock()) {
app.quit();
return;
}
app.on('second-instance', () => showWindow());
main().catch((err) => {
console.error(err);
app.quit();
});
}
/**
@@ -142,13 +193,13 @@ function setClickThrough(enabled) {
}
function setAutostart(enabled) {
app.setLoginItemSettings({ openAtLogin: enabled, args: [] });
applyAutostart(enabled);
buildTrayMenu();
}
function buildTrayMenu() {
if (!tray) return;
const autostart = app.getLoginItemSettings().openAtLogin;
const autostart = getAutostart();
tray.setContextMenu(
Menu.buildFromTemplate([
@@ -171,7 +222,7 @@ function buildTrayMenu() {
click: (item) => setClickThrough(item.checked),
},
{
label: 'Mit Windows starten',
label: IS_LINUX ? 'Mit dem System starten' : 'Mit Windows starten',
type: 'checkbox',
checked: autostart,
click: (item) => setAutostart(item.checked),
@@ -186,7 +237,9 @@ function buildTrayMenu() {
}
function createTray() {
const icon = nativeImage.createFromPath(path.join(ROOT, 'assets', 'tray.png'));
// In der GNOME-Leiste wirkt das 16-px-Symbol ausgefranst — dort das größere.
const file = IS_LINUX ? 'tray@32.png' : 'tray.png';
const icon = nativeImage.createFromPath(path.join(ROOT, 'assets', file));
tray = new Tray(icon);
tray.setToolTip('Claude Live Dashboard');
tray.on('click', toggleWindow);
@@ -297,7 +350,8 @@ function registerIpc() {
config,
meta: {
configPath: CONFIG_FILE,
autostart: app.getLoginItemSettings().openAtLogin,
autostart: getAutostart(),
platform: process.platform,
},
}));
@@ -366,8 +420,3 @@ app.on('window-all-closed', () => {});
app.on('before-quit', () => {
if (collector) collector.stop();
});
main().catch((err) => {
console.error(err);
app.quit();
});
+3
View File
@@ -20,6 +20,7 @@ const el = {
opacityOut: $('opacity-out'),
clickThrough: $('clickThrough'),
autostart: $('autostart'),
autostartLabel: $('autostart-label'),
oauthEnabled: $('oauthEnabled'),
oauthEndpoint: $('oauthEndpoint'),
@@ -80,6 +81,8 @@ function fill(config, meta) {
el.opacityOut.textContent = `${opacity} %`;
el.clickThrough.checked = Boolean(config.clickThrough);
el.autostart.checked = Boolean(meta.autostart);
el.autostartLabel.textContent =
meta.platform === 'win32' ? 'Mit Windows starten' : 'Mit dem System starten';
const oauth = config.oauth || {};
el.oauthEnabled.checked = oauth.enabled !== false;
+1 -1
View File
@@ -36,7 +36,7 @@
</div>
<div class="row">
<label for="autostart">Mit Windows starten</label>
<label for="autostart" id="autostart-label">Automatisch starten</label>
<div class="control">
<input type="checkbox" id="autostart" />
<span class="hint">Wird sofort übernommen, unabhängig vom Speichern.</span>
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Startet das Dashboard aus dem Quellcode. Gegenstück zu start.cmd; unter
# Ubuntu liegt Node im PATH, deshalb genügt die Abhängigkeitsprüfung.
set -euo pipefail
cd "$(dirname "$0")"
if ! command -v node >/dev/null 2>&1; then
echo "Node wurde nicht gefunden. Installiere es z.B. mit: sudo apt install nodejs npm"
exit 1
fi
if [ ! -d node_modules/electron ]; then
echo "Abhaengigkeiten werden installiert..."
npm install
fi
exec node_modules/.bin/electron . "$@"
+3 -2
View File
@@ -116,9 +116,10 @@ function main() {
console.log(`geschrieben: ${file}`);
}
// Fenster- und Anwendungssymbol
// Fenster- und Anwendungssymbol. 512 px, weil electron-builder daraus die
// Linux-Symbolgrößen herunterrechnet und mindestens 256 px verlangt.
const file = path.join(OUT_DIR, 'icon.png');
fs.writeFileSync(file, makePng(256, ringShader(256, orange)));
fs.writeFileSync(file, makePng(512, ringShader(512, orange)));
console.log(`geschrieben: ${file}`);
}