280 lines
9.2 KiB
TypeScript
280 lines
9.2 KiB
TypeScript
// --- 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<string, unknown>,
|
|
): Promise<Record<string, unknown>> {
|
|
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<Attrs> = () => {
|
|
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 ---
|