// --- BEGIN UNIFI THEME --- // UniFi-style device detail page. // This file REPLACES device-page.ts in the router without modifying the // original, preserving upstream merge-friendliness. // All TR-069 parameter rendering is delegated to the original device-page // component; this wrapper adds the UniFi header/action-bar on top. import { ClosureComponent, Children } from "mithril"; import { m } from "./components.ts"; import { icon } from "./tailwind-utility-components.ts"; import { device as deviceConfig } from "./config.ts"; import * as store from "./store.ts"; import * as notifications from "./notifications.ts"; import { queueTask } from "./task-queue.ts"; import Expression from "../lib/common/expression.ts"; import Path from "../lib/common/path.ts"; import { statusBadge, getDeviceStatus, getDeviceName, getDeviceModel, getParam, timeAgo, } from "./unifi-components.ts"; export function init( args: Record, ): Promise> { if (!window.authorizer.hasAccess("devices", 2)) { return Promise.reject( new Error("You are not authorized to view this page"), ); } return Promise.resolve({ deviceId: args.id, deviceFilter: new Expression.Binary( "=", new Expression.Parameter(Path.parse("DeviceID.ID")), new Expression.Literal(args.id as string), ), }); } interface Attrs { deviceId: string; deviceFilter: Expression; } // ─── KV pair helper ──────────────────────────────────────────────────────── function kv(label: string, value: string | null): Children { return m("div", [ m("div.unifi-detail-kv__label", label), m( `div.unifi-detail-kv__value${value == null ? ".empty" : ""}`, value ?? "—", ), ]); } // ─── Action helper ───────────────────────────────────────────────────────── function actionBtn( label: string, className: string, disabled: boolean, onClick: () => void, ): Children { return m( `button.unifi-btn.${className}`, { disabled, onclick: onClick }, label, ); } // ─── Component ───────────────────────────────────────────────────────────── export const component: ClosureComponent = () => { let actionsPending = false; return { view: (vnode) => { const deviceId = vnode.attrs.deviceId; document.title = `${deviceId} — Devices — GenieACS`; const dev = store.fetch("devices", vnode.attrs.deviceFilter); // While loading if (!dev.value || !dev.value.length) { if (!dev.fulfilling) { return m( "p", { style: "color:#e53935;font-weight:bold;" }, `No such device: ${deviceId}`, ); } return m("loading", { queries: [dev] }, m("div", { style: "height:100px" })); } const device = dev.value[0]; const status = getDeviceStatus(device); const name = getDeviceName(device); const model = getDeviceModel(device); const lastInform = device["_lastInform"] as number | undefined; const canWrite = window.authorizer.hasAccess("devices", 3); // Key TR-069 fields const serial = getParam(device, "DeviceID.SerialNumber") ?? getParam( device, "InternetGatewayDevice.DeviceInfo.SerialNumber", "Device.DeviceInfo.SerialNumber", ); const firmware = getParam( device, "InternetGatewayDevice.DeviceInfo.SoftwareVersion", "Device.DeviceInfo.SoftwareVersion", ); const wanIp = getParam( device, "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress", "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress", "Device.IP.Interface.1.IPv4Address.1.IPAddress", ); const ssid = getParam( device, "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", "Device.WiFi.SSID.1.SSID", ); const hardwareVersion = getParam( device, "InternetGatewayDevice.DeviceInfo.HardwareVersion", "Device.DeviceInfo.HardwareVersion", ); const uptime = getParam( device, "InternetGatewayDevice.DeviceInfo.UpTime", "Device.DeviceInfo.UpTime", ); // ── Router icon ──────────────────────────────────────────────────── const routerIcon = m( "div.unifi-device-detail-header__icon", m(icon, { name: "nav-devices", class: "h-7 w-7" }), ); // ── Header bar ───────────────────────────────────────────────────── const header = m("div.unifi-device-detail-header", [ // Back link m( "a.unifi-back-link", { href: "#!/devices" }, m(icon, { name: "nav-back", class: "h-4 w-4" }), " Back to Devices", ), m("div.unifi-device-detail-header__top", [ routerIcon, m("div.unifi-device-detail-header__info", [ m("h1.unifi-device-detail-header__name", name), m("div.unifi-device-detail-header__meta", [ m(statusBadge, { status }), model && m("span", model), serial && m("span", `S/N: ${serial}`), lastInform && m( "span", { title: new Date(lastInform).toLocaleString() }, `Last seen: ${timeAgo(lastInform)}`, ), ]), ]), // Actions canWrite && m("div.unifi-device-detail-header__actions", [ actionBtn( "Reboot", "unifi-btn--secondary", actionsPending, () => { actionsPending = true; queueTask({ name: "reboot", device: deviceId }); notifications.push( "success", `Reboot task queued for ${name}`, ); setTimeout(() => { actionsPending = false; m.redraw(); }, 2000); }, ), actionBtn( "Factory Reset", "unifi-btn--danger", actionsPending, () => { if ( !confirm( `Factory reset ${name}? This will erase all configuration.`, ) ) return; actionsPending = true; queueTask({ name: "factoryReset", device: deviceId }); notifications.push( "success", `Factory reset task queued for ${name}`, ); setTimeout(() => { actionsPending = false; m.redraw(); }, 2000); }, ), actionBtn( "Refresh", "unifi-btn--secondary", actionsPending, () => { actionsPending = true; store.setTimestamp(Date.now()); setTimeout(() => { actionsPending = false; m.redraw(); }, 1500); }, ), ]), ]), // Key-value grid m("div.unifi-device-detail-header__kv", [ kv("WAN IP", wanIp), kv("WiFi SSID", ssid), kv("Firmware", firmware), kv("Hardware", hardwareVersion), uptime != null && kv( "Uptime", (() => { const s = Number(uptime); if (isNaN(s)) return uptime; const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m2 = Math.floor((s % 3600) / 60); return [d && `${d}d`, h && `${h}h`, `${m2}m`] .filter(Boolean) .join(" "); })(), ), kv("Device ID", deviceId), ]), ]); // ── Original device-page content (all configured panels) ──────────── const conf = deviceConfig; const cmps = []; for (const c of Object.values(conf)) { cmps.push( m.context( { device: dev.value[0], deviceQuery: dev }, store.evaluateExpression(c["type"], {}).value as string, c as any, ), ); } const content = m("div.unifi-device-content", [ m("div.device-page", cmps), ]); return m("div", [header, content]); }, }; }; // --- END UNIFI THEME ---