implementação de mudança de interface

Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
This commit is contained in:
rodrigo committed 2026-03-30 09:53:11 -03:00
1 parent d445aa03cf
commit d4e1b297ae
24 files changed
+1982 -47

No files matched your search

+286
View File
@@ -0,0 +1,286 @@
// --- BEGIN UNIFI THEME ---
// Reusable UniFi-style UI components for GenieACS
// These are new files and do not conflict with upstream components.
import { ClosureComponent, Children } from "mithril";
import { m } from "./components.ts";
// ─── Status helpers ────────────────────────────────────────────────────────
export type DeviceStatus = "online" | "warning" | "offline" | "unknown";
/**
* Determines the online status of a device based on its _lastInform timestamp.
* < 5 min → online
* < 15 min → warning
* ≥ 15 min → offline
* missing → unknown
*/
export function getDeviceStatus(
device: Record<string, unknown>,
): DeviceStatus {
const lastInform = device["_lastInform"] as number | undefined;
if (!lastInform) return "unknown";
const age = Date.now() - lastInform;
if (age < 5 * 60 * 1000) return "online";
if (age < 15 * 60 * 1000) return "warning";
return "offline";
}
const STATUS_LABEL: Record<DeviceStatus, string> = {
online: "Online",
warning: "Recent",
offline: "Offline",
unknown: "Never",
};
// ─── TR-069 parameter accessor ─────────────────────────────────────────────
/**
* Safely reads the first value from a TR-069 parameter object.
* Tries multiple path keys and returns the first non-null hit.
*/
export function getParam(
device: Record<string, unknown>,
...paths: string[]
): string | null {
for (const path of paths) {
const entry = device[path];
if (entry == null) continue;
if (
typeof entry === "object" &&
(entry as Record<string, unknown>)["value"] != null
) {
const v = (entry as Record<string, unknown[]>)["value"];
if (Array.isArray(v) && v[0] != null) return String(v[0]);
}
}
return null;
}
/**
* Returns a human-readable device name (alias → serial → device ID).
*/
export function getDeviceName(device: Record<string, unknown>): string {
return (
getParam(device, "_deviceId._Alias") ??
getParam(device, "DeviceID.SerialNumber") ??
(device["DeviceID.ID"] as string) ??
"Unknown Device"
);
}
/**
* Returns the device model string (ProductClass → ModelName → Manufacturer).
*/
export function getDeviceModel(device: Record<string, unknown>): string {
const mfr = getParam(device, "DeviceID.Manufacturer") ?? "";
const product =
getParam(
device,
"DeviceID.ProductClass",
"InternetGatewayDevice.DeviceInfo.ModelName",
"Device.DeviceInfo.ModelName",
) ?? "";
return [mfr, product].filter(Boolean).join(" ") || "Unknown Model";
}
/**
* Returns a relative human-readable time label (e.g. "2 minutes ago").
*/
export function timeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000);
if (seconds < 5) return "just now";
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
// ─── StatusDot ─────────────────────────────────────────────────────────────
interface StatusDotAttrs {
status: DeviceStatus;
}
export const statusDot: ClosureComponent<StatusDotAttrs> = () => ({
view: (vnode) =>
m(`span.unifi-status-dot.unifi-status-dot--${vnode.attrs.status}`),
});
// ─── StatusBadge ───────────────────────────────────────────────────────────
interface StatusBadgeAttrs {
status: DeviceStatus;
}
export const statusBadge: ClosureComponent<StatusBadgeAttrs> = () => ({
view: (vnode) => {
const s = vnode.attrs.status;
return m(
`span.unifi-status-badge.unifi-status-badge--${s}`,
m(`span.unifi-status-dot.unifi-status-dot--${s}`, {
style: "width:7px;height:7px;box-shadow:none;",
}),
STATUS_LABEL[s],
);
},
});
// ─── StatCard ──────────────────────────────────────────────────────────────
interface StatCardAttrs {
label: string;
value: number | string;
colorClass?: string;
}
export const statCard: ClosureComponent<StatCardAttrs> = () => ({
view: (vnode) =>
m(
"div.unifi-stat-card",
m("div.unifi-stat-card__label", vnode.attrs.label),
m(
`div.unifi-stat-card__value${vnode.attrs.colorClass ? "." + vnode.attrs.colorClass : ""}`,
vnode.attrs.value,
),
),
});
// ─── DeviceCard ────────────────────────────────────────────────────────────
interface DeviceCardAttrs {
device: Record<string, unknown>;
onclick?: (e: MouseEvent) => void;
}
export const deviceCard: ClosureComponent<DeviceCardAttrs> = () => ({
view: (vnode) => {
const device = vnode.attrs.device;
const status = getDeviceStatus(device);
const name = getDeviceName(device);
const model = getDeviceModel(device);
const lastInform = device["_lastInform"] as number | undefined;
const deviceId = device["DeviceID.ID"] as string;
// WAN IP – try TR-098 then TR-181
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",
"Device.PPP.Interface.1.IPCPLocalIPAddress",
) ?? null;
// WiFi SSID – try TR-098 then TR-181
const ssid =
getParam(
device,
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.SSID",
"Device.WiFi.SSID.1.SSID",
"Device.WiFi.SSID.2.SSID",
) ?? null;
// Firmware
const firmware =
getParam(
device,
"InternetGatewayDevice.DeviceInfo.SoftwareVersion",
"Device.DeviceInfo.SoftwareVersion",
) ?? null;
// Tags
const tags = device["_tags"] as Record<string, boolean> | undefined;
const activeTags = tags
? Object.entries(tags)
.filter(([, v]) => v)
.map(([k]) => k)
: [];
function row(label: string, value: string | null): Children {
return m("div.unifi-device-card__row", [
m("span.unifi-device-card__row-label", label),
m(
`span.unifi-device-card__row-value${value == null ? ".empty" : ""}`,
value ?? "—",
),
]);
}
return m(
"div.unifi-device-card",
{ onclick: vnode.attrs.onclick },
[
// Header
m("div.unifi-device-card__header", [
m(statusDot, { status }),
m("div", { style: "flex:1;overflow:hidden;" }, [
m("p.unifi-device-card__name", { title: name }, name),
m("p.unifi-device-card__model", model),
]),
m(statusBadge, { status }),
]),
// Body
m("div.unifi-device-card__body", [
wanIp !== null && row("WAN", wanIp),
ssid !== null && row("WiFi", ssid),
firmware !== null && row("FW", firmware),
row(
"ID",
typeof deviceId === "string" ? deviceId.slice(0, 30) : null,
),
]),
// Footer
m("div.unifi-device-card__footer", [
m(
"span",
lastInform
? `Last seen ${timeAgo(lastInform)}`
: "Never connected",
),
activeTags.length > 0 &&
m(
"div.unifi-device-card__tags",
activeTags.slice(0, 3).map((t) => m("span.unifi-tag", t)),
),
]),
],
);
},
});
// ─── PageSizeSelector ──────────────────────────────────────────────────────
interface PageSizeSelectorAttrs {
value: number;
options: number[];
onChange: (n: number) => void;
}
export const pageSizeSelector: ClosureComponent<PageSizeSelectorAttrs> = () => ({
view: (vnode) =>
m("div.unifi-page-size-selector", [
m("span", "Per page:"),
m(
"select",
{
value: vnode.attrs.value,
onchange: (e: Event) =>
vnode.attrs.onChange(
Number((e.target as HTMLSelectElement).value),
),
},
vnode.attrs.options.map((n) =>
m("option", { value: n, selected: n === vnode.attrs.value }, String(n)),
),
),
]),
});
// --- END UNIFI THEME ---