Update-Pruefung gegen Gitea-Releases ergaenzen
Die App prueft ca. 60 s nach dem Start einmalig, ob auf dem Gitea-Server ein neueres Release vorliegt. Bei einem Treffer erscheint eine Systembenachrichtigung, und ein Symbol im Titelbalken bleibt sichtbar, bis aktualisiert wird -- Klick auf beides oeffnet die Release-Seite im Browser. Ueber die neue Einstellung "Nach Updates suchen" abschaltbar (Standard: aktiv).
This commit is contained in:
+23
-2
@@ -74,6 +74,7 @@ Position wird gemerkt.
|
|||||||
| Element | Bedeutung |
|
| Element | Bedeutung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Statuspunkt | Zeigt den Zustand der Datenquellen, siehe unten |
|
| Statuspunkt | Zeigt den Zustand der Datenquellen, siehe unten |
|
||||||
|
| Pfeil-Symbol (nur bei neuem Release) | Öffnet die Release-Seite im Browser |
|
||||||
| Zahnrad | Öffnet die Einstellungen |
|
| Zahnrad | Öffnet die Einstellungen |
|
||||||
| `–` | Blendet das Fenster aus (Rückkehr über das Tray-Symbol) |
|
| `–` | Blendet das Fenster aus (Rückkehr über das Tray-Symbol) |
|
||||||
|
|
||||||
@@ -121,12 +122,19 @@ Das Zahnrad öffnet den Einstellungsdialog:
|
|||||||
der Anmeldung.
|
der Anmeldung.
|
||||||
- **Serverabfrage** — kann abgeschaltet werden, falls die Anzeige rein lokal
|
- **Serverabfrage** — kann abgeschaltet werden, falls die Anzeige rein lokal
|
||||||
arbeiten soll (dann sind alle Werte Näherungen statt exakter Zahlen).
|
arbeiten soll (dann sind alle Werte Näherungen statt exakter Zahlen).
|
||||||
|
- **Nach Updates suchen** — prüft rund eine Minute nach dem Start einmalig,
|
||||||
|
ob auf dem Projekt-Server eine neuere Version vorliegt. Falls ja,
|
||||||
|
erscheint eine Systembenachrichtigung, und ein Pfeil-Symbol im
|
||||||
|
Titelbalken bleibt sichtbar, bis aktualisiert wird. Klick auf
|
||||||
|
Benachrichtigung oder Symbol öffnet die Release-Seite im Browser. Diese
|
||||||
|
Einstellung wirkt erst beim nächsten Start.
|
||||||
- **Bezugsgrößen, Gewichtung, Wochenfenster** — fortgeschrittene Feineinstellungen
|
- **Bezugsgrößen, Gewichtung, Wochenfenster** — fortgeschrittene Feineinstellungen
|
||||||
für den Fall, dass die Serverabfrage dauerhaft nicht zur Verfügung steht;
|
für den Fall, dass die Serverabfrage dauerhaft nicht zur Verfügung steht;
|
||||||
im Normalbetrieb müssen Sie hier nichts anpassen.
|
im Normalbetrieb müssen Sie hier nichts anpassen.
|
||||||
|
|
||||||
Änderungen wirken sofort, ein Neustart ist nicht nötig. `Strg`+`S` speichert,
|
Änderungen wirken sofort, ein Neustart ist nicht nötig (Ausnahme: „Nach
|
||||||
`Esc` schließt den Dialog ohne zu speichern.
|
Updates suchen", siehe oben). `Strg`+`S` speichert, `Esc` schließt den
|
||||||
|
Dialog ohne zu speichern.
|
||||||
|
|
||||||
## Tray-Menü
|
## Tray-Menü
|
||||||
|
|
||||||
@@ -154,6 +162,10 @@ und Beenden.
|
|||||||
selbst profitiert automatisch davon. Das geschieht ausschließlich auf
|
selbst profitiert automatisch davon. Das geschieht ausschließlich auf
|
||||||
Klick, nie automatisch im Hintergrund, und auch dabei wird kein Token
|
Klick, nie automatisch im Hintergrund, und auch dabei wird kein Token
|
||||||
protokolliert.
|
protokolliert.
|
||||||
|
- Ist „Nach Updates suchen" aktiviert, fragt die Anwendung rund eine Minute
|
||||||
|
nach dem Start einmalig den selbst gehosteten Gitea-Server
|
||||||
|
(`git.conatum.net`) nach der neuesten Versionsnummer ab — ohne
|
||||||
|
Zugriffstoken und ohne weitere Daten. Abschaltbar in den Einstellungen.
|
||||||
- Es findet keine Übertragung an Dritte statt.
|
- Es findet keine Übertragung an Dritte statt.
|
||||||
|
|
||||||
## Deinstallation
|
## Deinstallation
|
||||||
@@ -188,3 +200,12 @@ Abschnitt „Installation“ oben.
|
|||||||
**Nach einem Update sehe ich eine Benachrichtigung.** Das ist beabsichtigt:
|
**Nach einem Update sehe ich eine Benachrichtigung.** Das ist beabsichtigt:
|
||||||
Die Anwendung erkennt neue Versionen automatisch und weist einmalig darauf
|
Die Anwendung erkennt neue Versionen automatisch und weist einmalig darauf
|
||||||
hin. Ihre Einstellungen und Kalibrierung bleiben davon unberührt.
|
hin. Ihre Einstellungen und Kalibrierung bleiben davon unberührt.
|
||||||
|
|
||||||
|
**Ich bekomme eine Benachrichtigung über eine neue Version / ein
|
||||||
|
Pfeil-Symbol erscheint im Titelbalken.** Das ist eine andere Meldung als
|
||||||
|
oben: Die Anwendung hat rund eine Minute nach dem Start auf dem
|
||||||
|
Projekt-Server ein neueres Release gefunden. Ein Klick auf Benachrichtigung
|
||||||
|
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".
|
||||||
|
|||||||
@@ -276,6 +276,19 @@ npm run dist
|
|||||||
`npm version` legt standardmäßig auch einen Git-Commit und -Tag an; ohne das
|
`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.
|
genügt es, `version` in `package.json` von Hand zu ändern.
|
||||||
|
|
||||||
|
### Update-Prüfung beim Gitea-Server
|
||||||
|
|
||||||
|
Zusätzlich zur lokalen Erkennung oben prüft die Anwendung ca. 60 s nach dem
|
||||||
|
Start einmalig aktiv, ob auf dem selbst gehosteten Gitea-Server
|
||||||
|
(`git.conatum.net`, Repo `swinks/claude-live-dashboard`) ein neueres Release
|
||||||
|
vorliegt (`GET .../releases/latest`, öffentlich ohne Authentifizierung). Bei
|
||||||
|
einer neueren Version erscheint eine native Systembenachrichtigung, und ein
|
||||||
|
Symbol im Titelbalken (zwischen Endpunkt-Status und Zahnrad) bleibt
|
||||||
|
sichtbar, bis aktualisiert wird. Ein Klick auf Benachrichtigung oder Symbol
|
||||||
|
öffnet die Release-Seite im Standardbrowser. Über die Einstellung „Nach
|
||||||
|
Updates suchen" lässt sich dieser Netzwerkaufruf beim Start abschalten
|
||||||
|
(`config.json:updateCheck.enabled`, Standard `true`).
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -26,5 +26,9 @@
|
|||||||
"_oauth": "Serverabfrage für exakte Werte. Standardmäßig aktiv; mit enabled:false abschalten. endpoint nur setzen, um den Standard zu überschreiben.",
|
"_oauth": "Serverabfrage für exakte Werte. Standardmäßig aktiv; mit enabled:false abschalten. endpoint nur setzen, um den Standard zu überschreiben.",
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
},
|
||||||
|
"_updateCheck": "Prüft ca. 60 s nach dem Start einmalig beim Gitea-Server auf ein neueres Release. Standardmäßig aktiv; mit enabled:false abschalten.",
|
||||||
|
"updateCheck": {
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const SOURCE_LABEL = {
|
|||||||
oauth: 'Server',
|
oauth: 'Server',
|
||||||
app: 'App',
|
app: 'App',
|
||||||
refresh: 'Token',
|
refresh: 'Token',
|
||||||
|
update: 'Update',
|
||||||
};
|
};
|
||||||
|
|
||||||
let feedbackTimer = null;
|
let feedbackTimer = null;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
--good: #0ca30c;
|
--good: #0ca30c;
|
||||||
--warning: #fab219;
|
--warning: #fab219;
|
||||||
--critical: #d03b3b;
|
--critical: #d03b3b;
|
||||||
|
--update: #9b6bd6;
|
||||||
|
|
||||||
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
@@ -228,6 +229,10 @@ dd[data-severity="critical"] .status-label {
|
|||||||
color: var(--good);
|
color: var(--good);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-source[data-source="update"] {
|
||||||
|
color: var(--update);
|
||||||
|
}
|
||||||
|
|
||||||
.log-message {
|
.log-message {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ class Collector extends EventEmitter {
|
|||||||
this.errors = Array.isArray(store.data.errors) ? store.data.errors : [];
|
this.errors = Array.isArray(store.data.errors) ? store.data.errors : [];
|
||||||
this._localOk = true;
|
this._localOk = true;
|
||||||
this._lastOauthError = null;
|
this._lastOauthError = null;
|
||||||
|
// Für das Titelleisten-Symbol: bleibt für die gesamte Sitzung gesetzt,
|
||||||
|
// sobald ein neueres Release gefunden wurde — bewusst nicht gedrosselt
|
||||||
|
// wie die Notification (store.data.updateCheck.notifiedTag), das Symbol
|
||||||
|
// soll sichtbar bleiben, bis aktualisiert wird.
|
||||||
|
this._updateAvailable = null;
|
||||||
if (configError) this._note(configError, 'local');
|
if (configError) this._note(configError, 'local');
|
||||||
if (versionEvent) {
|
if (versionEvent) {
|
||||||
const msg =
|
const msg =
|
||||||
@@ -214,6 +219,24 @@ class Collector extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hält ein gefundenes neueres Release fest — im Ereignisprotokoll (einmalig
|
||||||
|
* pro Tag, dedupliziert über _note()) und als Live-State fürs
|
||||||
|
* Titelleisten-Symbol (state.update, bleibt für die Sitzung gesetzt).
|
||||||
|
* Ausgelöst einmalig ca. 60 s nach dem Start (siehe index.js/update-check.js).
|
||||||
|
*/
|
||||||
|
noteUpdateAvailable(tagName, htmlUrl) {
|
||||||
|
this._updateAvailable = { tagName, htmlUrl };
|
||||||
|
this._note(`Neue Version verfügbar: ${tagName} (${htmlUrl})`, 'update');
|
||||||
|
this._recompute().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hält einen gescheiterten Update-Check fest — stört den Betrieb nie, nur Protokoll. */
|
||||||
|
noteUpdateCheckError(message) {
|
||||||
|
this._note(`Update-Prüfung fehlgeschlagen: ${message}`, 'update');
|
||||||
|
this._recompute().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
async _poll() {
|
async _poll() {
|
||||||
if (this._polling) {
|
if (this._polling) {
|
||||||
this._pollAgain = true;
|
this._pollAgain = true;
|
||||||
@@ -411,6 +434,12 @@ class Collector extends EventEmitter {
|
|||||||
local: { ok: this._localOk },
|
local: { ok: this._localOk },
|
||||||
oauth: { ok: !oauthError, text: this.oauth.status().text },
|
oauth: { ok: !oauthError, text: this.oauth.status().text },
|
||||||
},
|
},
|
||||||
|
// Titelleisten-Symbol: bleibt sichtbar, bis die Sitzung endet oder
|
||||||
|
// aktualisiert wird — bewusst kein „gesehen"-Zustand wie bei der
|
||||||
|
// Notification (store.data.updateCheck.notifiedTag).
|
||||||
|
update: this._updateAvailable
|
||||||
|
? { available: true, tagName: this._updateAvailable.tagName, htmlUrl: this._updateAvailable.htmlUrl }
|
||||||
|
: { available: false, tagName: null, htmlUrl: null },
|
||||||
errors: this.errors.slice(),
|
errors: this.errors.slice(),
|
||||||
stats: { lastPollMs: this._lastPollMs },
|
stats: { lastPollMs: this._lastPollMs },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const { Store, loadConfig } = require('./store');
|
|||||||
const { Collector } = require('./collector');
|
const { Collector } = require('./collector');
|
||||||
const { CONFIG_FILE, STATE_FILE, USER_DATA_DIR, ensureUserConfig } = require('./paths');
|
const { CONFIG_FILE, STATE_FILE, USER_DATA_DIR, ensureUserConfig } = require('./paths');
|
||||||
const { getAutostart, setAutostart: applyAutostart } = require('./autostart');
|
const { getAutostart, setAutostart: applyAutostart } = require('./autostart');
|
||||||
|
const { checkForUpdate } = require('./update-check');
|
||||||
|
|
||||||
const IS_LINUX = process.platform === 'linux';
|
const IS_LINUX = process.platform === 'linux';
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ const SETTINGS_HEIGHT = 660;
|
|||||||
const ERRORS_WIDTH = 420;
|
const ERRORS_WIDTH = 420;
|
||||||
const ERRORS_HEIGHT = 480;
|
const ERRORS_HEIGHT = 480;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
||||||
let win = null;
|
let win = null;
|
||||||
let settingsWin = null;
|
let settingsWin = null;
|
||||||
let errorsWin = null;
|
let errorsWin = null;
|
||||||
@@ -410,6 +415,10 @@ function registerIpc() {
|
|||||||
if (!collector) return { ok: false, error: 'Anwendung startet noch' };
|
if (!collector) return { ok: false, error: 'Anwendung startet noch' };
|
||||||
return collector.refreshOauthToken();
|
return collector.refreshOauthToken();
|
||||||
});
|
});
|
||||||
|
ipcMain.on('open-release-page', () => {
|
||||||
|
const info = collector?.state?.update;
|
||||||
|
if (info?.available && info.htmlUrl) shell.openExternal(info.htmlUrl);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('settings:load', () => ({
|
ipcMain.handle('settings:load', () => ({
|
||||||
config,
|
config,
|
||||||
@@ -509,6 +518,45 @@ async function main() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[collector] Start fehlgeschlagen:', err);
|
console.error('[collector] Start fehlgeschlagen:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.updateCheck?.enabled !== false) {
|
||||||
|
const updateTimer = setTimeout(() => {
|
||||||
|
checkForUpdate(app.getVersion())
|
||||||
|
.then((result) => {
|
||||||
|
console.log(result ? `[update-check] neue Version: ${result.tagName}` : '[update-check] keine neuere Version verfügbar');
|
||||||
|
if (!result || !collector) return;
|
||||||
|
|
||||||
|
// Titelleisten-Symbol: sofort sichtbar, unabhängig von der
|
||||||
|
// Notification-Drossel unten.
|
||||||
|
collector.noteUpdateAvailable(result.tagName, result.htmlUrl);
|
||||||
|
|
||||||
|
// Notification nur einmal pro neu erschienenem Tag — sonst würde sie
|
||||||
|
// bei jedem Start erneut aufpoppen, solange der Nutzer nicht
|
||||||
|
// aktualisiert (die App läuft typischerweise per Autostart täglich).
|
||||||
|
const alreadyNotified = store.data.updateCheck?.notifiedTag === result.tagName;
|
||||||
|
store.data.updateCheck = { notifiedTag: result.tagName };
|
||||||
|
store.save();
|
||||||
|
if (alreadyNotified || !Notification.isSupported()) return;
|
||||||
|
|
||||||
|
const notification = new Notification({
|
||||||
|
title: 'Neue Version verfügbar',
|
||||||
|
body: `${result.tagName} steht bereit. Klicken, um die Release-Seite zu öffnen.`,
|
||||||
|
icon: path.join(ROOT, 'assets', 'icon.png'),
|
||||||
|
});
|
||||||
|
notification.on('click', () => shell.openExternal(result.htmlUrl));
|
||||||
|
notification.show();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn('[update-check] fehlgeschlagen:', err.message);
|
||||||
|
if (collector) collector.noteUpdateCheckError(err.message);
|
||||||
|
});
|
||||||
|
}, UPDATE_CHECK_DELAY_MS);
|
||||||
|
// Darf den App-Exit nie blockieren — anders als collector._timers wird
|
||||||
|
// dieser einmalige Main-Prozess-Timer nicht von collector.stop() erfasst.
|
||||||
|
if (updateTimer.unref) updateTimer.unref();
|
||||||
|
} else {
|
||||||
|
console.log('[update-check] übersprungen (in den Einstellungen deaktiviert)');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Das Widget lebt im Tray weiter, auch wenn kein Fenster offen ist.
|
// Das Widget lebt im Tray weiter, auch wenn kein Fenster offen ist.
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ const DEFAULT_STATE = {
|
|||||||
errors: [],
|
errors: [],
|
||||||
// Zuletzt gestartete App-Version, für die Neuinstallations-/Update-Erkennung.
|
// Zuletzt gestartete App-Version, für die Neuinstallations-/Update-Erkennung.
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
|
// Zuletzt per Notification gemeldeter Release-Tag — verhindert, dass
|
||||||
|
// dieselbe neue Version bei jedem Start erneut als Popup erscheint.
|
||||||
|
updateCheck: { notifiedTag: null },
|
||||||
};
|
};
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
@@ -54,6 +57,7 @@ class Store {
|
|||||||
window: { ...DEFAULT_STATE.window, ...(parsed.window || {}) },
|
window: { ...DEFAULT_STATE.window, ...(parsed.window || {}) },
|
||||||
oauth: { ...DEFAULT_STATE.oauth, ...(parsed.oauth || {}) },
|
oauth: { ...DEFAULT_STATE.oauth, ...(parsed.oauth || {}) },
|
||||||
oauthRefresh: { ...DEFAULT_STATE.oauthRefresh, ...(parsed.oauthRefresh || {}) },
|
oauthRefresh: { ...DEFAULT_STATE.oauthRefresh, ...(parsed.oauthRefresh || {}) },
|
||||||
|
updateCheck: { ...DEFAULT_STATE.updateCheck, ...(parsed.updateCheck || {}) },
|
||||||
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einmalige Update-Prüfung, ca. 60 s nach dem Start ausgelöst (siehe
|
||||||
|
* index.js). Fragt das öffentliche Gitea-API des eigenen Repos nach dem
|
||||||
|
* neuesten Release ab — kein Zugriffstoken nötig, der Endpunkt ist ohne
|
||||||
|
* Authentifizierung erreichbar (verifiziert per curl).
|
||||||
|
*
|
||||||
|
* Bewusst zustandslos und ohne Wiederholungslogik: Ein Check pro Sitzung
|
||||||
|
* genügt der Anforderung „einmalig prüfen". Persistenz (welches Release
|
||||||
|
* schon einmal per Notification gemeldet wurde) lebt bewusst außerhalb
|
||||||
|
* dieses Moduls, in index.js/store.js.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RELEASES_URL =
|
||||||
|
'https://git.conatum.net/api/v1/repos/swinks/claude-live-dashboard/releases/latest';
|
||||||
|
const ALLOWED_HOST = 'git.conatum.net';
|
||||||
|
const REQUEST_TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vergleicht zwei MAJOR.MINOR.PATCH-Versionen ohne Suffixe.
|
||||||
|
* @returns {number} negativ wenn a<b, 0 wenn gleich, positiv wenn a>b
|
||||||
|
*/
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
const pa = String(a).split('.').map(Number);
|
||||||
|
const pb = String(b).split('.').map(Number);
|
||||||
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||||
|
const diff = (pa[i] || 0) - (pb[i] || 0);
|
||||||
|
if (diff !== 0) return diff;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragt das neueste Release ab und vergleicht dessen Tag gegen currentVersion.
|
||||||
|
* Drafts und Pre-Releases werden ignoriert.
|
||||||
|
* @param {string} currentVersion z. B. app.getVersion() (ohne führendes "v")
|
||||||
|
* @returns {Promise<{tagName: string, htmlUrl: string} | null>}
|
||||||
|
*/
|
||||||
|
async function checkForUpdate(currentVersion) {
|
||||||
|
const url = new URL(RELEASES_URL);
|
||||||
|
if (url.protocol !== 'https:' || url.hostname !== ALLOWED_HOST) {
|
||||||
|
throw new Error('Gitea-Endpunkt zeigt nicht auf den erwarteten Host');
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const res = await fetch(RELEASES_URL, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.draft || json.prerelease) return null;
|
||||||
|
|
||||||
|
const remoteVersion = String(json.tag_name || '').replace(/^v/, '').trim();
|
||||||
|
if (!remoteVersion) return null;
|
||||||
|
if (compareVersions(remoteVersion, currentVersion) <= 0) return null;
|
||||||
|
|
||||||
|
return { tagName: String(json.tag_name), htmlUrl: json.html_url };
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(err.name === 'AbortError' ? 'Zeitüberschreitung' : err.message);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { checkForUpdate, compareVersions, RELEASES_URL };
|
||||||
@@ -24,4 +24,6 @@ contextBridge.exposeInMainWorld('dashboard', {
|
|||||||
openSettings: () => ipcRenderer.send('open-settings'),
|
openSettings: () => ipcRenderer.send('open-settings'),
|
||||||
/** Öffnet das Fehler-/Ereignis-Log. */
|
/** Öffnet das Fehler-/Ereignis-Log. */
|
||||||
openErrorLog: () => ipcRenderer.send('open-error-log'),
|
openErrorLog: () => ipcRenderer.send('open-error-log'),
|
||||||
|
/** Öffnet die Release-Seite eines gefundenen neuen Updates im Browser. */
|
||||||
|
openReleasePage: () => ipcRenderer.send('open-release-page'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const el = {
|
|||||||
planLabel: $('plan-label'),
|
planLabel: $('plan-label'),
|
||||||
staleBadge: $('stale-badge'),
|
staleBadge: $('stale-badge'),
|
||||||
btnEndpointStatus: $('btn-endpoint-status'),
|
btnEndpointStatus: $('btn-endpoint-status'),
|
||||||
|
btnUpdateAvailable: $('btn-update-available'),
|
||||||
btnHide: $('btn-hide'),
|
btnHide: $('btn-hide'),
|
||||||
btnSettings: $('btn-settings'),
|
btnSettings: $('btn-settings'),
|
||||||
btnExpand: $('btn-expand'),
|
btnExpand: $('btn-expand'),
|
||||||
@@ -284,6 +285,15 @@ function renderEndpointStatus(s) {
|
|||||||
`Serverabfrage: ${ep.oauth.text || '—'}`;
|
`Serverabfrage: ${ep.oauth.text || '—'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Titelleisten-Symbol für ein gefundenes neues Release. */
|
||||||
|
function renderUpdateAvailable(s) {
|
||||||
|
const update = s.update || { available: false };
|
||||||
|
el.btnUpdateAvailable.hidden = !update.available;
|
||||||
|
if (update.available) {
|
||||||
|
el.btnUpdateAvailable.title = `Version ${update.tagName} verfügbar — Release-Seite öffnen`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -397,6 +407,7 @@ function render(s) {
|
|||||||
renderWeek(s.week, s.burn);
|
renderWeek(s.week, s.burn);
|
||||||
renderSpend(s.spend);
|
renderSpend(s.spend);
|
||||||
renderEndpointStatus(s);
|
renderEndpointStatus(s);
|
||||||
|
renderUpdateAvailable(s);
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
renderModels(s.block);
|
renderModels(s.block);
|
||||||
@@ -462,6 +473,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());
|
el.btnEndpointStatus.addEventListener('click', () => window.dashboard.openErrorLog());
|
||||||
|
el.btnUpdateAvailable.addEventListener('click', () => window.dashboard.openReleasePage());
|
||||||
|
|
||||||
window.dashboard.onState(render);
|
window.dashboard.onState(render);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<button class="icon-btn status" id="btn-endpoint-status" data-severity="ok" title="Endpunkt-Status" aria-label="Endpunkt-Status">
|
<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>
|
<span class="status-icon" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="icon-btn" id="btn-update-available" hidden title="Neues Release verfügbar" aria-label="Neues Release verfügbar">↑</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
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
--status-good: #0ca30c;
|
--status-good: #0ca30c;
|
||||||
--status-warning: #fab219;
|
--status-warning: #fab219;
|
||||||
--status-critical: #d03b3b;
|
--status-critical: #d03b3b;
|
||||||
|
--status-update: #9b6bd6;
|
||||||
|
|
||||||
/* Kategoriale Slots (dunkle Stufen), fest je Modellfamilie vergeben */
|
/* Kategoriale Slots (dunkle Stufen), fest je Modellfamilie vergeben */
|
||||||
--series-1: #3987e5;
|
--series-1: #3987e5;
|
||||||
@@ -152,6 +153,10 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btn-update-available {
|
||||||
|
color: var(--status-update);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Meter ───────────────────────────────────────────────────────────── */
|
/* ── Meter ───────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.meters {
|
.meters {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const el = {
|
|||||||
clickThrough: $('clickThrough'),
|
clickThrough: $('clickThrough'),
|
||||||
autostart: $('autostart'),
|
autostart: $('autostart'),
|
||||||
autostartLabel: $('autostart-label'),
|
autostartLabel: $('autostart-label'),
|
||||||
|
updateCheckEnabled: $('updateCheckEnabled'),
|
||||||
|
|
||||||
oauthEnabled: $('oauthEnabled'),
|
oauthEnabled: $('oauthEnabled'),
|
||||||
oauthEndpoint: $('oauthEndpoint'),
|
oauthEndpoint: $('oauthEndpoint'),
|
||||||
@@ -84,6 +85,9 @@ function fill(config, meta) {
|
|||||||
el.autostartLabel.textContent =
|
el.autostartLabel.textContent =
|
||||||
meta.platform === 'win32' ? 'Mit Windows starten' : 'Mit dem System starten';
|
meta.platform === 'win32' ? 'Mit Windows starten' : 'Mit dem System starten';
|
||||||
|
|
||||||
|
const updateCheck = config.updateCheck || {};
|
||||||
|
el.updateCheckEnabled.checked = updateCheck.enabled !== false;
|
||||||
|
|
||||||
const oauth = config.oauth || {};
|
const oauth = config.oauth || {};
|
||||||
el.oauthEnabled.checked = oauth.enabled !== false;
|
el.oauthEnabled.checked = oauth.enabled !== false;
|
||||||
el.oauthEndpoint.value = oauth.endpoint || '';
|
el.oauthEndpoint.value = oauth.endpoint || '';
|
||||||
@@ -137,6 +141,7 @@ function collect() {
|
|||||||
|
|
||||||
patch.opacity = Number(el.opacity.value) / 100;
|
patch.opacity = Number(el.opacity.value) / 100;
|
||||||
patch.clickThrough = el.clickThrough.checked;
|
patch.clickThrough = el.clickThrough.checked;
|
||||||
|
patch.updateCheck = { enabled: el.updateCheckEnabled.checked };
|
||||||
|
|
||||||
patch.oauth = { enabled: el.oauthEnabled.checked };
|
patch.oauth = { enabled: el.oauthEnabled.checked };
|
||||||
const endpoint = el.oauthEndpoint.value.trim();
|
const endpoint = el.oauthEndpoint.value.trim();
|
||||||
|
|||||||
@@ -42,6 +42,17 @@
|
|||||||
<span class="hint">Wird sofort übernommen, unabhängig vom Speichern.</span>
|
<span class="hint">Wird sofort übernommen, unabhängig vom Speichern.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label for="updateCheckEnabled">Nach Updates suchen</label>
|
||||||
|
<div class="control">
|
||||||
|
<input type="checkbox" id="updateCheckEnabled" />
|
||||||
|
<span class="hint">
|
||||||
|
Prüft rund eine Minute nach dem Start einmalig auf dem
|
||||||
|
Projekt-Server, ob eine neuere Version vorliegt.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="group">
|
<section class="group">
|
||||||
|
|||||||
Reference in New Issue
Block a user