303 lines
9.1 KiB
TypeScript
303 lines
9.1 KiB
TypeScript
import {
|
||
app,
|
||
BrowserWindow,
|
||
Menu,
|
||
Tray,
|
||
dialog,
|
||
nativeImage,
|
||
shell,
|
||
} from "electron";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
import { configPath, loadConfig, saveConfig, type TrayConfig } from "./config";
|
||
import { pingAgent } from "./viaplay";
|
||
import { refreshAllSilent, refreshDueSilent } from "./jobs";
|
||
|
||
let tray: Tray | null = null;
|
||
let settingsWin: BrowserWindow | null = null;
|
||
let cfg: TrayConfig = loadConfig();
|
||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||
let busy = false;
|
||
let lastPollAt: string | null = null;
|
||
let lastDueCount = 0;
|
||
|
||
const POLL_MS = 10 * 60 * 1000; // elke 10 min: vraag server wie bijna verloopt
|
||
const WITHIN_MINUTES = 90; // vernieuw als token < 90 min geldig is
|
||
|
||
function resolveAsset(...parts: string[]): string {
|
||
const candidates = [
|
||
path.join(__dirname, "..", "assets", ...parts),
|
||
path.join(app.getAppPath(), "assets", ...parts),
|
||
path.join(process.resourcesPath, "assets", ...parts),
|
||
];
|
||
for (const p of candidates) {
|
||
if (fs.existsSync(p)) return p;
|
||
}
|
||
return candidates[0];
|
||
}
|
||
|
||
function loadTrayImage(): Electron.NativeImage {
|
||
for (const name of ["tray-icon-32.png", "tray-icon-16.png", "icon.png"] as const) {
|
||
const p = resolveAsset(name);
|
||
if (!fs.existsSync(p)) continue;
|
||
let img = nativeImage.createFromPath(p);
|
||
if (img.isEmpty()) continue;
|
||
const { width } = img.getSize();
|
||
// Windows systeemvak: 16–32 px; groter schalen ziet er wazig/onzichtbaar uit
|
||
if (width > 32) img = img.resize({ width: 16, height: 16 });
|
||
return img;
|
||
}
|
||
return nativeImage.createEmpty();
|
||
}
|
||
|
||
function statusTooltip(): string {
|
||
const parts = ["Media Cluster Token Tray (auto)"];
|
||
if (busy) parts.push("Bezig met stille refresh…");
|
||
parts.push(`Poll: elke 10 min · drempel ${WITHIN_MINUTES} min`);
|
||
if (lastPollAt) {
|
||
parts.push(`Laatste check: ${new Date(lastPollAt).toLocaleString("nl-NL")}`);
|
||
}
|
||
if (lastDueCount > 0) parts.push(`Laatst due: ${lastDueCount} account(s)`);
|
||
if (cfg.lastPushSummary) parts.push(cfg.lastPushSummary);
|
||
if (cfg.lastError) parts.push(`Fout: ${cfg.lastError}`);
|
||
return parts.join("\n");
|
||
}
|
||
|
||
function rebuildMenu() {
|
||
if (!tray) return;
|
||
const menu = Menu.buildFromTemplate([
|
||
{
|
||
label: busy ? "Bezig…" : "Nu checken (server → due)",
|
||
enabled: !busy,
|
||
click: () => void runAuto(true),
|
||
},
|
||
{
|
||
label: "Alles vernieuwen (stil)",
|
||
enabled: !busy,
|
||
click: () => void runManualAll(true),
|
||
},
|
||
{
|
||
label: "Alles vernieuwen (zichtbaar)",
|
||
enabled: !busy,
|
||
click: () => void runManualAll(false),
|
||
},
|
||
{ type: "separator" },
|
||
{ label: "Verbinding testen", click: () => void testConnection() },
|
||
{ label: "Instellingen…", click: () => openSettings() },
|
||
{
|
||
label: "Configmap openen",
|
||
click: () => void shell.showItemInFolder(configPath()),
|
||
},
|
||
{ type: "separator" },
|
||
{ label: "Afsluiten", click: () => app.quit() },
|
||
]);
|
||
tray.setContextMenu(menu);
|
||
tray.setToolTip(statusTooltip());
|
||
}
|
||
|
||
function openSettings() {
|
||
if (settingsWin && !settingsWin.isDestroyed()) {
|
||
settingsWin.focus();
|
||
return;
|
||
}
|
||
settingsWin = new BrowserWindow({
|
||
width: 520,
|
||
height: 360,
|
||
title: "Token Tray — instellingen",
|
||
autoHideMenuBar: true,
|
||
webPreferences: { contextIsolation: true, nodeIntegration: false },
|
||
});
|
||
const html = `<!doctype html>
|
||
<html lang="nl"><head><meta charset="utf-8"/><title>Instellingen</title>
|
||
<style>
|
||
body{font-family:Segoe UI,system-ui,sans-serif;margin:1.25rem;background:#12141a;color:#e8eaed}
|
||
label{display:block;margin:0.75rem 0 0.25rem;font-size:0.85rem;opacity:.85}
|
||
input{width:100%;box-sizing:border-box;padding:0.5rem 0.6rem;border-radius:6px;border:1px solid #333;background:#1c1f28;color:#fff}
|
||
button{margin-top:1.25rem;padding:0.55rem 1rem;border:0;border-radius:6px;background:#3b82f6;color:#fff;font-weight:600;cursor:pointer}
|
||
p{font-size:0.85rem;opacity:.75;line-height:1.4}
|
||
</style></head><body>
|
||
<h2 style="margin-top:0">Token Tray (auto)</h2>
|
||
<p>Server houdt JWT-expiry bij. Deze PC pollt elke 10 min en logt stil in via Chrome als tokens bijna verlopen. Accounts + wachtwoorden staan in Admin → Scripts.</p>
|
||
<label>API URL</label>
|
||
<input id="apiUrl" value="${escapeHtml(cfg.apiUrl)}" />
|
||
<label>Tray key (X-Tray-Key)</label>
|
||
<input id="trayKey" value="${escapeHtml(cfg.trayKey)}" />
|
||
<button id="save">Opslaan</button>
|
||
<script>
|
||
document.getElementById('save').onclick = () => {
|
||
const payload = {
|
||
apiUrl: document.getElementById('apiUrl').value,
|
||
trayKey: document.getElementById('trayKey').value,
|
||
};
|
||
document.title = 'SAVE:' + encodeURIComponent(JSON.stringify(payload));
|
||
};
|
||
</script>
|
||
</body></html>`;
|
||
settingsWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
||
settingsWin.on("page-title-updated", (e, title) => {
|
||
if (!title.startsWith("SAVE:")) return;
|
||
e.preventDefault();
|
||
try {
|
||
const payload = JSON.parse(decodeURIComponent(title.slice(5))) as {
|
||
apiUrl: string;
|
||
trayKey: string;
|
||
};
|
||
cfg.apiUrl = payload.apiUrl.trim().replace(/\/$/, "") || cfg.apiUrl;
|
||
cfg.trayKey = payload.trayKey.trim();
|
||
saveConfig(cfg);
|
||
rebuildMenu();
|
||
startPolling();
|
||
void dialog.showMessageBox({
|
||
type: "info",
|
||
message: "Opgeslagen",
|
||
detail: "Auto-poll draait (elke 10 min, alleen bijna-verlopen tokens).",
|
||
});
|
||
settingsWin?.close();
|
||
} catch (err) {
|
||
void dialog.showErrorBox("Opslaan mislukt", String(err));
|
||
}
|
||
});
|
||
settingsWin.on("closed", () => {
|
||
settingsWin = null;
|
||
});
|
||
}
|
||
|
||
function escapeHtml(s: string): string {
|
||
return s
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
/** Automatische poll: alleen due accounts, geen dialoog bij succes. */
|
||
async function runAuto(showDialogIfManual: boolean) {
|
||
if (busy) return;
|
||
if (!cfg.trayKey.trim()) {
|
||
if (showDialogIfManual) openSettings();
|
||
return;
|
||
}
|
||
busy = true;
|
||
rebuildMenu();
|
||
try {
|
||
const { fetchRefreshDue } = await import("./jobs");
|
||
const due = await fetchRefreshDue(cfg, WITHIN_MINUTES);
|
||
lastPollAt = new Date().toISOString();
|
||
lastDueCount = due.length;
|
||
if (!due.length) {
|
||
cfg = loadConfig();
|
||
cfg.lastPushSummary = "Auto: niets te vernieuwen";
|
||
saveConfig(cfg);
|
||
if (showDialogIfManual) {
|
||
void dialog.showMessageBox({
|
||
type: "info",
|
||
message: "Niets te doen",
|
||
detail: `Geen tokens die binnen ${WITHIN_MINUTES} min verlopen.`,
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
const summary = await refreshDueSilent(cfg, {
|
||
headless: true,
|
||
withinMinutes: WITHIN_MINUTES,
|
||
notifyOnFailOnly: !showDialogIfManual,
|
||
});
|
||
cfg = loadConfig();
|
||
if (showDialogIfManual) {
|
||
void dialog.showMessageBox({
|
||
type: summary.failed ? "warning" : "info",
|
||
message: "Auto-refresh klaar",
|
||
detail: `${summary.ok} ok · ${summary.failed} fout\n${summary.errors.slice(0, 3).join("\n")}`,
|
||
});
|
||
}
|
||
} catch (err) {
|
||
if (showDialogIfManual) {
|
||
void dialog.showErrorBox("Auto-check mislukt", String(err));
|
||
}
|
||
} finally {
|
||
busy = false;
|
||
rebuildMenu();
|
||
}
|
||
}
|
||
|
||
async function runManualAll(headless: boolean) {
|
||
if (busy) return;
|
||
if (!cfg.trayKey.trim()) {
|
||
openSettings();
|
||
return;
|
||
}
|
||
busy = true;
|
||
rebuildMenu();
|
||
try {
|
||
const summary = await refreshAllSilent(cfg, { headless, notify: true });
|
||
cfg = loadConfig();
|
||
void dialog.showMessageBox({
|
||
type: summary.failed ? "warning" : "info",
|
||
message: "Refresh klaar",
|
||
detail: [
|
||
`${summary.ok} ok · ${summary.failed} fout · ${summary.skipped} zonder wachtwoord`,
|
||
...summary.errors.slice(0, 5),
|
||
].join("\n"),
|
||
});
|
||
} catch (err) {
|
||
void dialog.showErrorBox("Refresh mislukt", String(err));
|
||
} finally {
|
||
busy = false;
|
||
rebuildMenu();
|
||
}
|
||
}
|
||
|
||
async function testConnection() {
|
||
if (!cfg.trayKey.trim()) {
|
||
openSettings();
|
||
return;
|
||
}
|
||
try {
|
||
const ok = await pingAgent(cfg);
|
||
void dialog.showMessageBox({
|
||
type: ok ? "info" : "warning",
|
||
message: ok ? "Verbinding OK" : "Geen geldige tray-key of API",
|
||
detail: cfg.apiUrl,
|
||
});
|
||
} catch (err) {
|
||
void dialog.showErrorBox("Verbinding mislukt", String(err));
|
||
}
|
||
}
|
||
|
||
function createTray() {
|
||
const img = loadTrayImage();
|
||
tray = new Tray(img);
|
||
tray.setToolTip("Media Cluster Token Tray");
|
||
rebuildMenu();
|
||
tray.on("double-click", () => void runAuto(true));
|
||
}
|
||
|
||
function startPolling() {
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
// Direct één check, daarna elke 10 min
|
||
void runAuto(false);
|
||
pollTimer = setInterval(() => void runAuto(false), POLL_MS);
|
||
}
|
||
|
||
app.whenReady().then(() => {
|
||
try {
|
||
app.setLoginItemSettings({
|
||
openAtLogin: true,
|
||
openAsHidden: true,
|
||
});
|
||
} catch {
|
||
/* ignore — niet op alle platforms beschikbaar */
|
||
}
|
||
cfg = loadConfig();
|
||
createTray();
|
||
if (!cfg.trayKey) openSettings();
|
||
else startPolling();
|
||
});
|
||
|
||
app.on("window-all-closed", () => {
|
||
/* tray blijft */
|
||
});
|
||
|
||
app.on("before-quit", () => {
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
});
|