Auto-refresh tokens based on server-tracked JWT expiry.

Tray polls refresh-due every 10 minutes and silently renews accounts within 90 minutes of expiry.
This commit is contained in:
Jos Vooges | STH 2026-09-23 00:12:03 +02:00
parent 4b4f0abcff
commit c6eec07de5
9 changed files with 373 additions and 102 deletions

View file

@ -5,6 +5,7 @@ import { resolveTokenTraySecret } from "../settings/token-tray";
import { decryptAccount as decryptViaplay } from "../viaplay/accounts";
import { decryptAccount as decryptF1tv } from "../f1tv/accounts";
import { prisma } from "../database/client";
import { jwtExpiryIso, tokenNeedsRefresh } from "./token-expiry";
async function requireTrayKey(request: FastifyRequest, config: Config) {
const secret = await resolveTokenTraySecret(
@ -32,70 +33,125 @@ export type AgentAccountRow = {
password: string | null;
hasTokens: boolean;
enabled: boolean;
tokenExpiresAt: string | null;
refreshDue: boolean;
};
async function listAgentAccounts(
config: Config,
opts?: { onlyDue?: boolean; withinMinutes?: number }
): Promise<AgentAccountRow[]> {
const withinMs = Math.max(5, opts?.withinMinutes ?? 90) * 60_000;
const accounts: AgentAccountRow[] = [];
const vpRow = await prisma.integrationSetting.findUnique({ where: { id: "viaplay" } });
if (vpRow?.configJson) {
const { parseAccountsJson } = await import("../viaplay/accounts");
try {
const parsed = JSON.parse(vpRow.configJson) as { accounts?: unknown };
for (const a of parseAccountsJson(parsed.accounts)) {
if (a.enabled === false) continue;
const creds = decryptViaplay(a, config.SESSION_SECRET);
const exp =
a.tokenExpiresAt ||
jwtExpiryIso(creds.accessToken) ||
null;
const refreshDue =
!!creds.password &&
tokenNeedsRefresh(exp, withinMs);
accounts.push({
provider: "viaplay",
id: a.id,
email: a.email,
label: a.label || a.email,
password: creds.password,
hasTokens: !!(creds.session && creds.accessToken),
enabled: true,
tokenExpiresAt: exp,
refreshDue,
});
}
} catch {
/* ignore */
}
}
const f1Row = await prisma.integrationSetting.findUnique({ where: { id: "f1tv" } });
if (f1Row?.configJson) {
const { parseAccountsJson } = await import("../f1tv/accounts");
try {
const parsed = JSON.parse(f1Row.configJson) as { accounts?: unknown };
for (const a of parseAccountsJson(parsed.accounts)) {
if (a.enabled === false) continue;
const creds = decryptF1tv(a, config.SESSION_SECRET);
const exp =
a.tokenExpiresAt ||
[jwtExpiryIso(creds.ascendontoken), jwtExpiryIso(creds.entitlementtoken)]
.filter(Boolean)
.sort()[0] ||
null;
const refreshDue =
!!creds.password &&
tokenNeedsRefresh(exp, withinMs);
accounts.push({
provider: "f1tv",
id: a.id,
email: a.email,
label: a.label || a.email,
password: creds.password,
hasTokens: !!(creds.ascendontoken && creds.entitlementtoken),
enabled: true,
tokenExpiresAt: exp,
refreshDue,
});
}
} catch {
/* ignore */
}
}
if (opts?.onlyDue) return accounts.filter((a) => a.refreshDue && a.password);
return accounts;
}
export function registerAgentRoutes(app: FastifyInstance, config: Config) {
app.get("/api/v1/agent/status", async (request) => {
await requireTrayKey(request, config);
const due = await listAgentAccounts(config, { onlyDue: true, withinMinutes: 90 });
return {
ok: true,
trayEnabled: true,
providers: ["viaplay", "f1tv"],
refreshDueCount: due.length,
};
});
/** Accounts + wachtwoorden voor stille browser-login (alleen met tray-key). */
/** Alle accounts (+ wachtwoorden) voor de tray. */
app.get("/api/v1/agent/accounts", async (request) => {
await requireTrayKey(request, config);
const accounts: AgentAccountRow[] = [];
const q = request.query as { withinMinutes?: string };
const withinMinutes = Number(q.withinMinutes) || 90;
const accounts = await listAgentAccounts(config, { withinMinutes });
return { accounts, withinMinutes };
});
const vpRow = await prisma.integrationSetting.findUnique({ where: { id: "viaplay" } });
if (vpRow?.configJson) {
const { parseAccountsJson } = await import("../viaplay/accounts");
try {
const parsed = JSON.parse(vpRow.configJson) as { accounts?: unknown };
for (const a of parseAccountsJson(parsed.accounts)) {
if (a.enabled === false) continue;
const creds = decryptViaplay(a, config.SESSION_SECRET);
accounts.push({
provider: "viaplay",
id: a.id,
email: a.email,
label: a.label || a.email,
password: creds.password,
hasTokens: !!(creds.session && creds.accessToken),
enabled: true,
});
}
} catch {
/* ignore corrupt */
}
}
const f1Row = await prisma.integrationSetting.findUnique({ where: { id: "f1tv" } });
if (f1Row?.configJson) {
const { parseAccountsJson } = await import("../f1tv/accounts");
try {
const parsed = JSON.parse(f1Row.configJson) as { accounts?: unknown };
for (const a of parseAccountsJson(parsed.accounts)) {
if (a.enabled === false) continue;
const creds = decryptF1tv(a, config.SESSION_SECRET);
accounts.push({
provider: "f1tv",
id: a.id,
email: a.email,
label: a.label || a.email,
password: creds.password,
hasTokens: !!(creds.ascendontoken && creds.entitlementtoken),
enabled: true,
});
}
} catch {
/* ignore */
}
}
return { accounts };
/**
* Alleen accounts die binnenkort verlopen / geen token hebben.
* Tray pollt dit en vernieuwt stil — zonder handmatige actie.
*/
app.get("/api/v1/agent/refresh-due", async (request) => {
await requireTrayKey(request, config);
const q = request.query as { withinMinutes?: string };
const withinMinutes = Number(q.withinMinutes) || 90;
const accounts = await listAgentAccounts(config, {
onlyDue: true,
withinMinutes,
});
return {
withinMinutes,
count: accounts.length,
accounts,
};
});
app.post("/api/v1/agent/viaplay/tokens", async (request) => {

View file

@ -0,0 +1,26 @@
/** Decode JWT exp → ISO string (geen signature-check; alleen voor expiry-planning). */
export function jwtExpiryIso(token: string | null | undefined): string | null {
if (!token?.trim()) return null;
const raw = token.trim().replace(/^(VIAPLAY-AT|MTG-AT|Bearer)\s+/i, "");
try {
const payload = JSON.parse(
Buffer.from(raw.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString()
) as { exp?: number };
if (!payload.exp || !Number.isFinite(payload.exp)) return null;
return new Date(payload.exp * 1000).toISOString();
} catch {
return null;
}
}
/** True als token ontbreekt, onleesbaar is, of binnen `withinMs` verloopt. */
export function tokenNeedsRefresh(
expiresAtIso: string | null | undefined,
withinMs: number,
nowMs = Date.now()
): boolean {
if (!expiresAtIso) return true;
const t = Date.parse(expiresAtIso);
if (!Number.isFinite(t)) return true;
return t - nowMs <= withinMs;
}

View file

@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import { decryptSecret, encryptSecret } from "../security/crypto";
import { jwtExpiryIso } from "../agent/token-expiry";
export type F1tvAccountHealth = "ok" | "dead" | "unknown";
@ -16,6 +17,8 @@ export type F1tvAccountStored = {
healthStatus?: F1tvAccountHealth;
healthCheckedAt?: string;
healthError?: string;
/** ISO — earliest JWT exp van play-tokens. */
tokenExpiresAt?: string;
};
export type F1tvAccountPublic = {
@ -31,6 +34,7 @@ export type F1tvAccountPublic = {
healthStatus: F1tvAccountHealth;
healthCheckedAt: string | null;
healthError: string | null;
tokenExpiresAt: string | null;
};
export type F1tvAccountCreds = {
@ -70,6 +74,7 @@ export function accountToPublic(
healthStatus: account.healthStatus ?? "unknown",
healthCheckedAt: account.healthCheckedAt ?? null,
healthError: account.healthError ?? null,
tokenExpiresAt: account.tokenExpiresAt ?? null,
};
}
@ -116,6 +121,12 @@ export function createAccountStored(
if (!password && !ascendontoken && !entitlementtoken) {
throw new Error("Wachtwoord of tokens verplicht");
}
const expA = jwtExpiryIso(ascendontoken);
const expB = jwtExpiryIso(entitlementtoken);
const tokenExpiresAt =
[expA, expB]
.filter(Boolean)
.sort()[0] ?? undefined;
return {
id: input.id?.trim() || randomUUID(),
label: (input.label ?? "").trim() || email,
@ -129,6 +140,7 @@ export function createAccountStored(
: undefined,
sessionId: input.sessionId?.trim() || undefined,
enabled: input.enabled !== false,
tokenExpiresAt,
};
}
@ -159,6 +171,8 @@ export function parseAccountsJson(raw: unknown): F1tvAccountStored[] {
healthCheckedAt:
typeof row.healthCheckedAt === "string" ? row.healthCheckedAt : undefined,
healthError: typeof row.healthError === "string" ? row.healthError : undefined,
tokenExpiresAt:
typeof row.tokenExpiresAt === "string" ? row.tokenExpiresAt : undefined,
});
}
return out;

View file

@ -1,5 +1,5 @@
import { prisma } from "../database/client";
import { encryptSecret } from "../security/crypto";
import { decryptSecret, encryptSecret } from "../security/crypto";
import { AppError } from "../security/errors";
import {
accountHasPlayTokens,
@ -231,6 +231,26 @@ export async function updateF1tvAccount(
sessionId = input.sessionId.trim() || undefined;
}
let tokenExpiresAt = current.tokenExpiresAt;
if (input.clearTokens) {
tokenExpiresAt = undefined;
} else if (input.ascendontoken?.trim() || input.entitlementtoken?.trim()) {
const { jwtExpiryIso } = await import("../agent/token-expiry");
const a = jwtExpiryIso(
input.ascendontoken?.trim() ||
(current.ascendontokenEnc
? decryptSecret(current.ascendontokenEnc, sessionSecret)
: null)
);
const b = jwtExpiryIso(
input.entitlementtoken?.trim() ||
(current.entitlementtokenEnc
? decryptSecret(current.entitlementtokenEnc, sessionSecret)
: null)
);
tokenExpiresAt = [a, b].filter(Boolean).sort()[0] ?? undefined;
}
config.accounts[idx] = {
...current,
email,
@ -245,6 +265,7 @@ export async function updateF1tvAccount(
ascendontokenEnc,
entitlementtokenEnc,
sessionId,
tokenExpiresAt,
};
await persist(config, existing?.enabled ?? true);
return getF1tvSettingsPublic();

View file

@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import { decryptSecret, encryptSecret } from "../security/crypto";
import { jwtExpiryIso } from "../agent/token-expiry";
export type ViaplayAccountHealth = "ok" | "dead" | "unknown";
@ -21,6 +22,8 @@ export type ViaplayAccountStored = {
subscriptionSummary?: string;
partnerName?: string;
overviewCheckedAt?: string;
/** ISO — accessToken JWT exp (voor tray auto-refresh). */
tokenExpiresAt?: string;
};
export type ViaplayAccountPublic = {
@ -39,6 +42,7 @@ export type ViaplayAccountPublic = {
subscriptionSummary: string | null;
partnerName: string | null;
overviewCheckedAt: string | null;
tokenExpiresAt: string | null;
};
export type ViaplayAccountCreds = {
@ -83,6 +87,7 @@ export function accountToPublic(
subscriptionSummary: account.subscriptionSummary ?? null,
partnerName: account.partnerName ?? null,
overviewCheckedAt: account.overviewCheckedAt ?? null,
tokenExpiresAt: account.tokenExpiresAt ?? null,
};
}
@ -153,6 +158,7 @@ export function createAccountStored(
? encryptSecret(input.persistentLogin.trim(), sessionSecret)
: undefined,
enabled: input.enabled !== false,
tokenExpiresAt: accessToken ? jwtExpiryIso(accessToken) ?? undefined : undefined,
};
}
@ -192,6 +198,8 @@ export function parseAccountsJson(raw: unknown): ViaplayAccountStored[] {
partnerName: typeof row.partnerName === "string" ? row.partnerName : undefined,
overviewCheckedAt:
typeof row.overviewCheckedAt === "string" ? row.overviewCheckedAt : undefined,
tokenExpiresAt:
typeof row.tokenExpiresAt === "string" ? row.tokenExpiresAt : undefined,
});
}
return out;

View file

@ -320,6 +320,14 @@ export async function updateViaplayAccount(
persistentLoginEnc = encryptSecret(input.persistentLogin.trim(), sessionSecret);
}
let tokenExpiresAt = current.tokenExpiresAt;
if (input.clearCookies) {
tokenExpiresAt = undefined;
} else if (input.accessToken?.trim()) {
const { jwtExpiryIso } = await import("../agent/token-expiry");
tokenExpiresAt = jwtExpiryIso(input.accessToken) ?? undefined;
}
config.accounts[idx] = {
...current,
email,
@ -335,6 +343,7 @@ export async function updateViaplayAccount(
passwordEnc: input.password?.trim()
? encryptSecret(input.password.trim(), sessionSecret)
: current.passwordEnc,
tokenExpiresAt,
};
await persist(config, existing?.enabled ?? true);
const tokensChanged = !!(

View file

@ -18,11 +18,16 @@ pnpm.cmd run build
pnpm.cmd run start
```
Menu:
## Auto-modus (hands-off)
- **Stil vernieuwen** — headless Chromium klikt inloggen, pusht tokens
- **Vernieuwen (zichtbaar)** — zelfde flow met venster (debug)
- Automatisch elke **3 uur**
Zolang de tray op de Windows-PC draait:
1. Server onthoudt JWT-expiry per account
2. Tray pollt elke **10 min** `GET /api/v1/agent/refresh-due`
3. Alleen als een token **&lt; 90 min** geldig is (of ontbreekt): stille Chrome-login + push
4. Jij hoeft niets te klikken — alleen PC/tray aan laten
Handmatig blijft beschikbaar: *Nu checken* / *Alles vernieuwen*.
## API (tray-key)

View file

@ -12,8 +12,26 @@ export type AgentAccount = {
password: string | null;
hasTokens: boolean;
enabled: boolean;
tokenExpiresAt?: string | null;
refreshDue?: boolean;
};
export async function fetchRefreshDue(
cfg: TrayConfig,
withinMinutes = 90
): Promise<AgentAccount[]> {
const res = await fetch(
`${cfg.apiUrl}/api/v1/agent/refresh-due?withinMinutes=${withinMinutes}`,
{ headers: { "X-Tray-Key": cfg.trayKey } }
);
const data = (await res.json().catch(() => ({}))) as {
error?: { message?: string };
accounts?: AgentAccount[];
};
if (!res.ok) throw new Error(data.error?.message || `HTTP ${res.status}`);
return data.accounts ?? [];
}
export async function fetchAgentAccounts(cfg: TrayConfig): Promise<AgentAccount[]> {
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/accounts`, {
headers: { "X-Tray-Key": cfg.trayKey },
@ -91,40 +109,48 @@ export type RefreshSummary = {
errors: string[];
};
/** Headless UI-login voor alle accounts met wachtwoord. */
export async function refreshAllSilent(
async function refreshOne(
cfg: TrayConfig,
opts?: { headless?: boolean; providers?: Array<"viaplay" | "f1tv"> }
acc: AgentAccount,
headless: boolean
): Promise<void> {
if (!acc.password?.trim()) throw new Error("Geen wachtwoord");
if (acc.provider === "viaplay") {
const tokens = await loginViaplayUi({
accountId: acc.id,
email: acc.email,
password: acc.password,
headless,
});
await pushViaplay(cfg, acc.email, acc.label, tokens);
return;
}
const tokens = await loginF1tvUi({
accountId: acc.id,
email: acc.email,
password: acc.password,
headless,
});
await pushF1tv(cfg, acc.email, acc.label, tokens);
}
/** Headless UI-login voor gegeven accounts (of alle met wachtwoord). */
export async function refreshAccountsSilent(
cfg: TrayConfig,
accounts: AgentAccount[],
opts?: { headless?: boolean; notify?: boolean }
): Promise<RefreshSummary> {
const headless = opts?.headless !== false;
const providers = new Set(opts?.providers ?? ["viaplay", "f1tv"]);
const accounts = await fetchAgentAccounts(cfg);
const notify = opts?.notify !== false;
const summary: RefreshSummary = { ok: 0, failed: 0, skipped: 0, errors: [] };
for (const acc of accounts) {
if (!providers.has(acc.provider)) continue;
if (!acc.password?.trim()) {
summary.skipped += 1;
continue;
}
try {
if (acc.provider === "viaplay") {
const tokens = await loginViaplayUi({
accountId: acc.id,
email: acc.email,
password: acc.password,
headless,
});
await pushViaplay(cfg, acc.email, acc.label, tokens);
} else {
const tokens = await loginF1tvUi({
accountId: acc.id,
email: acc.email,
password: acc.password,
headless,
});
await pushF1tv(cfg, acc.email, acc.label, tokens);
}
await refreshOne(cfg, acc, headless);
summary.ok += 1;
} catch (err) {
summary.failed += 1;
@ -135,16 +161,54 @@ export async function refreshAllSilent(
}
cfg.lastPushAt = new Date().toISOString();
cfg.lastPushSummary = `Stil: ${summary.ok} ok · ${summary.failed} fout · ${summary.skipped} overgeslagen`;
cfg.lastPushSummary = `Auto: ${summary.ok} ok · ${summary.failed} fout · ${summary.skipped} overgeslagen`;
cfg.lastError = summary.errors[0];
saveConfig(cfg);
if (Notification.isSupported()) {
if (notify && Notification.isSupported() && (summary.ok > 0 || summary.failed > 0)) {
new Notification({
title: "Token Tray",
title: summary.failed ? "Token Tray — fouten" : "Token Tray",
body: cfg.lastPushSummary,
}).show();
}
return summary;
}
export async function refreshAllSilent(
cfg: TrayConfig,
opts?: { headless?: boolean; providers?: Array<"viaplay" | "f1tv">; notify?: boolean }
): Promise<RefreshSummary> {
const providers = new Set(opts?.providers ?? ["viaplay", "f1tv"]);
const all = await fetchAgentAccounts(cfg);
return refreshAccountsSilent(
cfg,
all.filter((a) => providers.has(a.provider)),
opts
);
}
/** Alleen accounts die de server als bijna-verlopen markeert. */
export async function refreshDueSilent(
cfg: TrayConfig,
opts?: { headless?: boolean; withinMinutes?: number; notifyOnFailOnly?: boolean }
): Promise<RefreshSummary> {
const due = await fetchRefreshDue(cfg, opts?.withinMinutes ?? 90);
if (!due.length) {
cfg.lastPushSummary = "Auto: niets te vernieuwen";
saveConfig(cfg);
return { ok: 0, failed: 0, skipped: 0, errors: [] };
}
return refreshAccountsSilent(cfg, due, {
headless: opts?.headless !== false,
notify: opts?.notifyOnFailOnly ? false : true,
}).then((summary) => {
if (opts?.notifyOnFailOnly && summary.failed > 0 && Notification.isSupported()) {
new Notification({
title: "Token Tray — refresh mislukt",
body: summary.errors.slice(0, 2).join(" · "),
}).show();
}
return summary;
});
}

View file

@ -9,22 +9,29 @@ import {
} from "electron";
import { configPath, loadConfig, saveConfig, type TrayConfig } from "./config";
import { pingAgent } from "./viaplay";
import { refreshAllSilent } from "./jobs";
import { refreshAllSilent, refreshDueSilent } from "./jobs";
let tray: Tray | null = null;
let settingsWin: BrowserWindow | null = null;
let cfg: TrayConfig = loadConfig();
let watchTimer: ReturnType<typeof setInterval> | null = null;
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 statusTooltip(): string {
const parts = ["Media Cluster Token Tray"];
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}`);
if (cfg.lastPushAt) {
parts.push(`Laatste push: ${new Date(cfg.lastPushAt).toLocaleString("nl-NL")}`);
}
return parts.join("\n");
}
@ -32,14 +39,19 @@ function rebuildMenu() {
if (!tray) return;
const menu = Menu.buildFromTemplate([
{
label: busy ? "Bezig…" : "Stil vernieuwen (alle accounts)",
label: busy ? "Bezig…" : "Nu checken (server → due)",
enabled: !busy,
click: () => void runSilent(true),
click: () => void runAuto(true),
},
{
label: "Vernieuwen (zichtbaar venster)",
label: "Alles vernieuwen (stil)",
enabled: !busy,
click: () => void runSilent(false),
click: () => void runManualAll(true),
},
{
label: "Alles vernieuwen (zichtbaar)",
enabled: !busy,
click: () => void runManualAll(false),
},
{ type: "separator" },
{ label: "Verbinding testen", click: () => void testConnection() },
@ -75,10 +87,9 @@ function openSettings() {
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}
code{font-size:0.8rem;background:#1c1f28;padding:2px 5px;border-radius:4px}
</style></head><body>
<h2 style="margin-top:0">Token Tray</h2>
<p>Accounts + wachtwoorden komen uit Admin (Scripts). Hier alleen API-URL + tray-key.</p>
<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>
@ -107,10 +118,11 @@ function openSettings() {
cfg.trayKey = payload.trayKey.trim();
saveConfig(cfg);
rebuildMenu();
startPolling();
void dialog.showMessageBox({
type: "info",
message: "Instellingen opgeslagen",
detail: `Accounts met wachtwoord staan in Admin → Scripts (Viaplay / F1TV).`,
message: "Opgeslagen",
detail: "Auto-poll draait (elke 10 min, alleen bijna-verlopen tokens).",
});
settingsWin?.close();
} catch (err) {
@ -130,7 +142,57 @@ function escapeHtml(s: string): string {
.replace(/'/g, "&#39;");
}
async function runSilent(headless: boolean) {
/** 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();
@ -139,7 +201,7 @@ async function runSilent(headless: boolean) {
busy = true;
rebuildMenu();
try {
const summary = await refreshAllSilent(cfg, { headless });
const summary = await refreshAllSilent(cfg, { headless, notify: true });
cfg = loadConfig();
void dialog.showMessageBox({
type: summary.failed ? "warning" : "info",
@ -181,21 +243,27 @@ function createTray() {
tray = new Tray(png.isEmpty() ? nativeImage.createEmpty() : png);
tray.setToolTip("Media Cluster Token Tray");
rebuildMenu();
tray.on("double-click", () => void runSilent(true));
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(() => {
cfg = loadConfig();
createTray();
if (!cfg.trayKey) openSettings();
// Elke 3 uur stil proberen (alleen accounts mét wachtwoord)
watchTimer = setInterval(() => void runSilent(true), 3 * 60 * 60 * 1000);
else startPolling();
});
app.on("window-all-closed", () => {
// Tray blijft actief
/* tray blijft */
});
app.on("before-quit", () => {
if (watchTimer) clearInterval(watchTimer);
if (pollTimer) clearInterval(pollTimer);
});