diff --git a/apps/admin-ui/src/app/agent/page.tsx b/apps/admin-ui/src/app/agent/page.tsx
index 28c5d85..1135cf0 100644
--- a/apps/admin-ui/src/app/agent/page.tsx
+++ b/apps/admin-ui/src/app/agent/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { Nav, useAuth } from "@/components/Nav";
@@ -25,20 +25,32 @@ function formatBytes(n: number): string {
export default function AgentPage() {
useAuth();
+ const fileRef = useRef(null);
const [meta, setMeta] = useState(null);
const [err, setErr] = useState(null);
+ const [msg, setMsg] = useState(null);
const [loading, setLoading] = useState(true);
+ const [version, setVersion] = useState("0.1.1");
+ const [uploading, setUploading] = useState(false);
+ const [progress, setProgress] = useState(null);
+ const [deleting, setDeleting] = useState(false);
const load = useCallback(async () => {
setLoading(true);
setErr(null);
try {
- const res = await fetch(`${MASTER_URL}/install/token-tray.json`, {
+ const res = await fetch("/api/v1/admin/install/token-tray", {
+ credentials: "include",
cache: "no-store",
});
- if (!res.ok) throw new Error(`Meta ophalen mislukt (${res.status})`);
- const data = (await res.json()) as TrayMeta;
+ const data = (await res.json()) as TrayMeta & {
+ error?: { message?: string };
+ };
+ if (!res.ok) {
+ throw new Error(data.error?.message || `Meta ophalen mislukt (${res.status})`);
+ }
setMeta(data);
+ if (data.version) setVersion(String(data.version));
} catch (e) {
setErr(e instanceof Error ? e.message : String(e));
setMeta(null);
@@ -51,6 +63,78 @@ export default function AgentPage() {
void load();
}, [load]);
+ function uploadFile(file: File | null) {
+ if (!file) return;
+ if (!file.name.toLowerCase().endsWith(".exe")) {
+ setErr("Kies een .exe (NSIS-installer)");
+ return;
+ }
+ setErr(null);
+ setMsg(null);
+ setUploading(true);
+ setProgress(0);
+
+ const fd = new FormData();
+ fd.append("file", file, file.name);
+ fd.append("version", version.trim() || "0.0.0");
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", "/api/v1/admin/install/token-tray");
+ xhr.withCredentials = true;
+ xhr.upload.onprogress = (ev) => {
+ if (!ev.lengthComputable) return;
+ setProgress(Math.round((ev.loaded / ev.total) * 100));
+ };
+ xhr.onload = () => {
+ setUploading(false);
+ setProgress(null);
+ if (fileRef.current) fileRef.current.value = "";
+ try {
+ const data = JSON.parse(xhr.responseText) as TrayMeta & {
+ error?: { message?: string };
+ };
+ if (xhr.status < 200 || xhr.status >= 300) {
+ setErr(data.error?.message || `Upload mislukt (${xhr.status})`);
+ return;
+ }
+ setMeta(data);
+ if (data.version) setVersion(String(data.version));
+ setMsg(
+ `Geüpload: v${data.version || "?"} · ${formatBytes(data.sizeBytes ?? 0)}`
+ );
+ } catch {
+ setErr(`Upload mislukt (${xhr.status})`);
+ }
+ };
+ xhr.onerror = () => {
+ setUploading(false);
+ setProgress(null);
+ setErr("Netwerkfout tijdens upload — proxy/timeout?");
+ };
+ xhr.send(fd);
+ }
+
+ async function removeInstaller() {
+ if (!confirm("Installer van Master verwijderen?")) return;
+ setDeleting(true);
+ setErr(null);
+ setMsg(null);
+ try {
+ const res = await fetch("/api/v1/admin/install/token-tray", {
+ method: "DELETE",
+ credentials: "include",
+ });
+ const data = (await res.json()) as TrayMeta & { error?: { message?: string } };
+ if (!res.ok) throw new Error(data.error?.message || "Verwijderen mislukt");
+ setMeta(data);
+ setMsg("Installer verwijderd");
+ } catch (e) {
+ setErr(e instanceof Error ? e.message : String(e));
+ } finally {
+ setDeleting(false);
+ }
+ }
+
const available = Boolean(meta?.available);
const downloadHref =
meta?.downloadUrl?.trim() || `${MASTER_URL}/install/token-tray-setup.exe`;
@@ -68,16 +152,82 @@ export default function AgentPage() {
stil via Chrome/Edge — hands-off zolang de PC aan staat.
-