Add Token Tray NSIS installer hosting and /agent download page.
Serve the Windows setup via /install, ship deploy/agent in Master, and document volume upload for the gitignored exe.
This commit is contained in:
parent
fcc816fa33
commit
36d02cab10
13 changed files with 362 additions and 13 deletions
151
apps/admin-ui/src/app/agent/page.tsx
Normal file
151
apps/admin-ui/src/app/agent/page.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Nav, useAuth } from "@/components/Nav";
|
||||
|
||||
const MASTER_URL = "https://master.vonas.nl";
|
||||
|
||||
type TrayMeta = {
|
||||
available?: boolean;
|
||||
version?: string | null;
|
||||
filename?: string;
|
||||
sizeBytes?: number;
|
||||
builtAt?: string | null;
|
||||
productName?: string;
|
||||
downloadUrl?: string;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
function formatBytes(n: number): string {
|
||||
if (!n || n <= 0) return "—";
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
|
||||
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export default function AgentPage() {
|
||||
useAuth();
|
||||
const [meta, setMeta] = useState<TrayMeta | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setErr(null);
|
||||
try {
|
||||
const res = await fetch(`${MASTER_URL}/install/token-tray.json`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) throw new Error(`Meta ophalen mislukt (${res.status})`);
|
||||
const data = (await res.json()) as TrayMeta;
|
||||
setMeta(data);
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
setMeta(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const available = Boolean(meta?.available);
|
||||
const downloadHref =
|
||||
meta?.downloadUrl?.trim() || `${MASTER_URL}/install/token-tray-setup.exe`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<main className="container">
|
||||
<header className="page-header">
|
||||
<div>
|
||||
<p className="page-kicker">Windows agent</p>
|
||||
<h1>Token Tray</h1>
|
||||
<p className="page-lead">
|
||||
Installeer de tray-app op een Windows-PC. Die vernieuwt Viaplay- en F1TV-tokens
|
||||
stil via Chrome/Edge — hands-off zolang de PC aan staat.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => void load()} disabled={loading}>
|
||||
{loading ? "Laden…" : "Vernieuwen"}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{err && <div className="error">{err}</div>}
|
||||
|
||||
<div className="section-head">
|
||||
<h2 className="section-title">Download</h2>
|
||||
<p className="muted">NSIS-installer (x64). SmartScreen kan waarschuwen — lokaal gebouwd, niet Store-gesigned.</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
{loading && !meta ? (
|
||||
<p className="muted">Status ophalen…</p>
|
||||
) : available ? (
|
||||
<>
|
||||
<p style={{ marginTop: 0 }}>
|
||||
<strong>{meta?.productName || "Media Cluster Token Tray"}</strong>
|
||||
{meta?.version ? (
|
||||
<span className="muted"> · v{meta.version}</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p className="muted" style={{ fontSize: "0.9rem" }}>
|
||||
{formatBytes(meta?.sizeBytes ?? 0)}
|
||||
{meta?.builtAt
|
||||
? ` · gebouwd ${new Date(meta.builtAt).toLocaleString("nl-NL")}`
|
||||
: ""}
|
||||
</p>
|
||||
<a className="btn" href={downloadHref} style={{ display: "inline-block", marginTop: 8 }}>
|
||||
Download installer
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p style={{ marginTop: 0 }}>Installer nog niet beschikbaar op Master.</p>
|
||||
<p className="muted" style={{ fontSize: "0.9rem" }}>
|
||||
{meta?.note ||
|
||||
"Bouw op Windows: apps/token-tray → pack:win → publish:installer, plaats token-tray-setup.exe in het agent-volume (of rebuild image met deploy/agent/), daarna deze pagina vernieuwen."}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="section-head" style={{ marginTop: "1.5rem" }}>
|
||||
<h2 className="section-title">Na installatie</h2>
|
||||
</div>
|
||||
<div className="card">
|
||||
<ol className="install-ol" style={{ margin: 0, paddingLeft: "1.25rem" }}>
|
||||
<li>Run de installer → tray-icoon in het systeemvak.</li>
|
||||
<li>
|
||||
Open <strong>Instellingen…</strong> in het tray-menu.
|
||||
</li>
|
||||
<li>
|
||||
Plak <strong>API URL</strong> + <strong>Tray key</strong> uit{" "}
|
||||
<Link href="/settings">Instellingen → Token Tray</Link>.
|
||||
</li>
|
||||
<li>
|
||||
Zorg dat Viaplay/F1TV-accounts een <strong>wachtwoord</strong> hebben onder{" "}
|
||||
<Link href="/scripts">Scripts</Link>.
|
||||
</li>
|
||||
<li>
|
||||
Laat de tray draaien (start mee met Windows). Poll elke 10 min; tokens < 90 min
|
||||
worden stil vernieuwd.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="section-head" style={{ marginTop: "1.5rem" }}>
|
||||
<h2 className="section-title">Vereisten</h2>
|
||||
</div>
|
||||
<div className="card">
|
||||
<ul style={{ margin: 0, paddingLeft: "1.25rem" }}>
|
||||
<li>Windows 10/11 x64</li>
|
||||
<li>Google Chrome of Microsoft Edge (Playwright channel)</li>
|
||||
<li>Netwerk naar {MASTER_URL}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -784,8 +784,9 @@ export default function SettingsPage() {
|
|||
<div className="section-head" style={{ marginTop: "1.5rem" }}>
|
||||
<h2 className="section-title">Token Tray</h2>
|
||||
<p className="muted">
|
||||
Key voor de Windows Token Tray-app (<code>apps/token-tray</code>). Plak API-URL +
|
||||
key in de tray-instellingen.
|
||||
Key voor de Windows Token Tray-app. Download de installer op{" "}
|
||||
<Link href="/agent">/agent</Link>, plak daarna API-URL + key in de
|
||||
tray-instellingen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ function installDir(): string {
|
|||
if (
|
||||
fs.existsSync(path.join(dir, "unraid.sh")) ||
|
||||
fs.existsSync(path.join(dir, "synology.sh")) ||
|
||||
fs.existsSync(path.join(dir, "upgrade-node.sh"))
|
||||
fs.existsSync(path.join(dir, "upgrade-node.sh")) ||
|
||||
fs.existsSync(path.join(dir, "token-tray-setup.exe")) ||
|
||||
fs.existsSync(path.join(dir, "agent", "token-tray-setup.exe"))
|
||||
) {
|
||||
return dir;
|
||||
}
|
||||
|
|
@ -21,6 +23,20 @@ function installDir(): string {
|
|||
return candidates[0];
|
||||
}
|
||||
|
||||
function tokenTrayPaths(dir: string): { exe: string; meta: string } {
|
||||
const nested = path.join(dir, "agent");
|
||||
if (fs.existsSync(path.join(nested, "token-tray-setup.exe"))) {
|
||||
return {
|
||||
exe: path.join(nested, "token-tray-setup.exe"),
|
||||
meta: path.join(nested, "token-tray.json"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
exe: path.join(dir, "token-tray-setup.exe"),
|
||||
meta: path.join(dir, "token-tray.json"),
|
||||
};
|
||||
}
|
||||
|
||||
function sendInstallScript(
|
||||
reply: { header: (k: string, v: string) => unknown; status: (c: number) => { send: (b: string) => unknown }; send: (b: string) => unknown },
|
||||
dir: string,
|
||||
|
|
@ -67,4 +83,53 @@ export async function registerInstallRoutes(app: FastifyInstance, config: Config
|
|||
return reply.send(fs.createReadStream(file));
|
||||
});
|
||||
}
|
||||
|
||||
app.get("/install/token-tray.json", async (_request, reply) => {
|
||||
const { exe, meta } = tokenTrayPaths(dir);
|
||||
const available = fs.existsSync(exe);
|
||||
let body: Record<string, unknown> = {
|
||||
available,
|
||||
version: null,
|
||||
filename: "token-tray-setup.exe",
|
||||
sizeBytes: 0,
|
||||
downloadUrl: `${publicUrl.replace(/\/$/, "")}/install/token-tray-setup.exe`,
|
||||
productName: "Media Cluster Token Tray",
|
||||
};
|
||||
if (fs.existsSync(meta)) {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(meta, "utf8")) as Record<string, unknown>;
|
||||
body = { ...body, ...parsed, available };
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
if (available) {
|
||||
body.sizeBytes = fs.statSync(exe).size;
|
||||
body.available = true;
|
||||
} else {
|
||||
body.available = false;
|
||||
body.sizeBytes = 0;
|
||||
}
|
||||
reply.header("Content-Type", "application/json; charset=utf-8");
|
||||
reply.header("Cache-Control", "no-store");
|
||||
return reply.send(body);
|
||||
});
|
||||
|
||||
app.get("/install/token-tray-setup.exe", async (_request, reply) => {
|
||||
const { exe } = tokenTrayPaths(dir);
|
||||
if (!fs.existsSync(exe)) {
|
||||
return reply
|
||||
.status(404)
|
||||
.send(
|
||||
"Token Tray installer not found — build with apps/token-tray pack:win + publish:installer, then redeploy."
|
||||
);
|
||||
}
|
||||
reply.header("Content-Type", "application/octet-stream");
|
||||
reply.header(
|
||||
"Content-Disposition",
|
||||
'attachment; filename="Media-Cluster-Token-Tray-Setup.exe"'
|
||||
);
|
||||
reply.header("Cache-Control", "no-store");
|
||||
return reply.send(fs.createReadStream(exe));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
5
apps/token-tray/.gitignore
vendored
Normal file
5
apps/token-tray/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
release/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
|
@ -2,22 +2,43 @@
|
|||
|
||||
Windows system-tray helper: **stille browser-UI login** voor Viaplay + F1TV (geen HTTP-login → geen cmd-CAPTCHA).
|
||||
|
||||
## Admin
|
||||
## Gebruikers (productie)
|
||||
|
||||
1. **Instellingen → Admin → Token Tray** → genereer key + kopieer API URL
|
||||
2. **Scripts → Viaplay / F1TV** → account met **e-mail + wachtwoord**
|
||||
3. Tokens plakken is optioneel (tray vult die)
|
||||
1. Open **https://admin.vonas.nl/agent** → download NSIS-installer
|
||||
2. Installeer → tray start mee met Windows
|
||||
3. **Instellingen → Token Tray** → kopieer API URL + key naar de tray
|
||||
4. **Scripts → Viaplay / F1TV** → accounts met e-mail + wachtwoord
|
||||
|
||||
## Tray
|
||||
## Dev (zonder installer)
|
||||
|
||||
```powershell
|
||||
cd apps\token-tray
|
||||
pnpm.cmd install
|
||||
pnpm.cmd exec playwright install chromium
|
||||
pnpm.cmd run build
|
||||
pnpm.cmd run start
|
||||
```
|
||||
|
||||
## Installer bouwen + publiceren
|
||||
|
||||
```powershell
|
||||
cd apps\token-tray
|
||||
pnpm.cmd install
|
||||
pnpm.cmd run pack:win
|
||||
pnpm.cmd run publish:installer
|
||||
```
|
||||
|
||||
Dat schrijft:
|
||||
|
||||
- `deploy/agent/token-tray-setup.exe`
|
||||
- `deploy/agent/token-tray.json`
|
||||
|
||||
Master serveert:
|
||||
|
||||
- `GET /install/token-tray-setup.exe`
|
||||
- `GET /install/token-tray.json`
|
||||
|
||||
De `.exe` staat in `.gitignore`. Op Dokploy: mount/upload naar volume `agent_install` → `/app/install/agent/`, of force-add + rebuild image.
|
||||
|
||||
## Auto-modus (hands-off)
|
||||
|
||||
Zolang de tray op de Windows-PC draait:
|
||||
|
|
@ -25,12 +46,13 @@ 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 **< 90 min** geldig is (of ontbreekt): stille Chrome-login + push
|
||||
4. Jij hoeft niets te klikken — alleen PC/tray aan laten
|
||||
4. Jij hoeft niets te klikken — alleen PC/tray aan laten
|
||||
|
||||
Handmatig blijft beschikbaar: *Nu checken* / *Alles vernieuwen*.
|
||||
|
||||
## API (tray-key)
|
||||
|
||||
- `GET /api/v1/agent/accounts` — accounts + wachtwoorden
|
||||
- `GET /api/v1/agent/refresh-due`
|
||||
- `POST /api/v1/agent/viaplay/tokens`
|
||||
- `POST /api/v1/agent/f1tv/tokens`
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Windows system tray helper to refresh Viaplay/F1TV session tokens",
|
||||
"author": "Vonas Media Cluster",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"dev": "tsc -p tsconfig.json && electron .",
|
||||
"start": "electron .",
|
||||
"postinstall": "node -e \"try{require('child_process').execSync('playwright install chromium',{stdio:'inherit'})}catch(e){console.warn('playwright chromium download skipped — tray gebruikt system Chrome/Edge')}\"",
|
||||
"pack:win": "npm run build && electron-builder --win dir"
|
||||
"pack:win": "pnpm run build && electron-builder --win nsis",
|
||||
"pack:win:dir": "pnpm run build && electron-builder --win dir",
|
||||
"publish:installer": "node scripts/publish-installer.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright": "^1.49.1"
|
||||
|
|
@ -23,6 +26,7 @@
|
|||
"build": {
|
||||
"appId": "nl.vonas.mediacluster.token-tray",
|
||||
"productName": "Media Cluster Token Tray",
|
||||
"copyright": "Copyright © Vonas",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
|
|
@ -30,9 +34,29 @@
|
|||
"dist/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extraMetadata": {
|
||||
"main": "dist/main.js"
|
||||
},
|
||||
"win": {
|
||||
"target": ["dir"],
|
||||
"signAndEditExecutable": false
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"signAndEditExecutable": false,
|
||||
"artifactName": "token-tray-setup.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": false,
|
||||
"createDesktopShortcut": false,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "Media Cluster Token Tray",
|
||||
"runAfterFinish": true,
|
||||
"uninstallDisplayName": "Media Cluster Token Tray",
|
||||
"deleteAppDataOnUninstall": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
apps/token-tray/scripts/publish-installer.cjs
Normal file
40
apps/token-tray/scripts/publish-installer.cjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Copy NSIS installer → deploy/agent/ for Docker/Dokploy serving via /install/token-tray-setup.exe
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const releaseDir = path.join(root, "release");
|
||||
const outDir = path.join(root, "..", "..", "deploy", "agent");
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
||||
|
||||
const candidates = [
|
||||
path.join(releaseDir, "token-tray-setup.exe"),
|
||||
...fs
|
||||
.readdirSync(releaseDir)
|
||||
.filter((f) => /setup.*\.exe$/i.test(f) || /token-tray.*\.exe$/i.test(f))
|
||||
.map((f) => path.join(releaseDir, f)),
|
||||
];
|
||||
|
||||
const src = candidates.find((f) => fs.existsSync(f));
|
||||
if (!src) {
|
||||
console.error("Geen installer gevonden in apps/token-tray/release/ — run: pnpm pack:win");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const destExe = path.join(outDir, "token-tray-setup.exe");
|
||||
fs.copyFileSync(src, destExe);
|
||||
const stat = fs.statSync(destExe);
|
||||
const meta = {
|
||||
version: pkg.version,
|
||||
filename: "token-tray-setup.exe",
|
||||
sizeBytes: stat.size,
|
||||
builtAt: new Date().toISOString(),
|
||||
productName: "Media Cluster Token Tray",
|
||||
available: true,
|
||||
};
|
||||
fs.writeFileSync(path.join(outDir, "token-tray.json"), JSON.stringify(meta, null, 2));
|
||||
console.log("Published:", destExe);
|
||||
console.log(meta);
|
||||
|
|
@ -254,6 +254,14 @@ function startPolling() {
|
|||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
try {
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
openAsHidden: true,
|
||||
});
|
||||
} catch {
|
||||
/* ignore — niet op alle platforms beschikbaar */
|
||||
}
|
||||
cfg = loadConfig();
|
||||
createTray();
|
||||
if (!cfg.trayKey) openSettings();
|
||||
|
|
|
|||
20
deploy/agent/.gitignore
vendored
Normal file
20
deploy/agent/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Token Tray Windows installer (NSIS)
|
||||
#
|
||||
# Build on Windows:
|
||||
# cd apps/token-tray
|
||||
# pnpm.cmd install
|
||||
# pnpm.cmd run pack:win
|
||||
# pnpm.cmd run publish:installer
|
||||
#
|
||||
# That writes:
|
||||
# deploy/agent/token-tray-setup.exe
|
||||
# deploy/agent/token-tray.json
|
||||
#
|
||||
# Master serves:
|
||||
# GET /install/token-tray-setup.exe
|
||||
# GET /install/token-tray.json
|
||||
# Admin page: https://admin.vonas.nl/agent
|
||||
|
||||
*.exe
|
||||
*.blockmap
|
||||
!.gitkeep
|
||||
0
deploy/agent/.gitkeep
Normal file
0
deploy/agent/.gitkeep
Normal file
9
deploy/agent/token-tray.json
Normal file
9
deploy/agent/token-tray.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"version": "0.0.0",
|
||||
"filename": "token-tray-setup.exe",
|
||||
"sizeBytes": 0,
|
||||
"builtAt": null,
|
||||
"productName": "Media Cluster Token Tray",
|
||||
"available": false,
|
||||
"note": "Run apps/token-tray pack:win + publish:installer on Windows, copy token-tray-setup.exe into the Dokploy agent_install volume (/app/install/agent/), then refresh /agent."
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ COPY --from=node-build /media-node-linux-arm64 /app/install/media-node-linux-arm
|
|||
COPY deploy/node/install-unraid.sh /app/install/unraid.sh
|
||||
COPY deploy/node/install-synology.sh /app/install/synology.sh
|
||||
COPY deploy/node/upgrade-node.sh /app/install/upgrade-node.sh
|
||||
COPY deploy/agent/ /app/install/agent/
|
||||
COPY apps/master-api/scripts/odido_cdm.py /app/scripts/odido_cdm.py
|
||||
|
||||
# Remove workspace protocol deps before npm install
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ services:
|
|||
volumes:
|
||||
# Persist Odido WVD.wvd (admin-upload → /data/odido/WVD.wvd)
|
||||
- odido_data:/data/odido
|
||||
# Optional: mount/upload Token Tray NSIS installer here as token-tray-setup.exe
|
||||
- agent_install:/app/install/agent
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
|
||||
interval: 15s
|
||||
|
|
@ -106,6 +108,7 @@ volumes:
|
|||
postgres_data:
|
||||
redis_data:
|
||||
odido_data:
|
||||
agent_install:
|
||||
|
||||
networks:
|
||||
media-cluster:
|
||||
|
|
|
|||
Loading…
Reference in a new issue