device page, fmt;

This commit is contained in:
lda
2026-04-12 21:47:23 +07:00 Unverified
parent bb813170d1
commit b576ba9a01
19 changed files with 356 additions and 162 deletions
+19 -17
View File
@@ -120,6 +120,12 @@ function chooseWakeTarget(device: DeviceRow): string {
: (device.macs[0] || device.ips[0] || "");
}
function summarize(values: string[]): string {
if (!values.length) return "-";
if (values.length === 1) return values[0];
return `${values[0]} (+${values.length - 1})`;
}
function loadHistory(): WakeEvent[] {
try {
const raw = window.localStorage.getItem(WAKE_HISTORY_KEY);
@@ -337,46 +343,42 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
<p className="muted">Showing {filtered.length} of {rows.length}</p>
{error && <pre className="error">{error}</pre>}
<div className="list device-list">
<div className="row plain device-row device-header" style={{ fontWeight: 600 }}>
<div className="row plain device-row device-header">
<span
className="sortable-col"
style={{ cursor: "pointer" }}
className="sortable-col device-cell"
onClick={() => setSort((s) => ({ key: "name", dir: s.key === "name" && s.dir === "asc" ? "desc" : "asc" }))}
>
Name {sort.key === "name" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
</span>
<span
className="sortable-col"
style={{ cursor: "pointer" }}
className="sortable-col device-cell"
onClick={() => setSort((s) => ({ key: "ip", dir: s.key === "ip" && s.dir === "asc" ? "desc" : "asc" }))}
>
IP {sort.key === "ip" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
</span>
<span
className="sortable-col"
style={{ cursor: "pointer" }}
className="sortable-col device-cell"
onClick={() => setSort((s) => ({ key: "mac", dir: s.key === "mac" && s.dir === "asc" ? "desc" : "asc" }))}
>
MAC {sort.key === "mac" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
</span>
<span
className="sortable-col"
style={{ cursor: "pointer" }}
className="sortable-col device-cell"
onClick={() => setSort((s) => ({ key: "presence", dir: s.key === "presence" && s.dir === "asc" ? "desc" : "asc" }))}
>
Presence {sort.key === "presence" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
</span>
<span>Interfaces</span>
<span></span>
<span className="device-cell">Interfaces</span>
<span className="device-cell device-action"></span>
</div>
{filtered.map((row) => (
<div className="row plain device-row" key={row.id}>
<span>{row.name}</span>
<span className="muted">{row.ips.join(", ") || "-"}</span>
<span className="muted">{row.macs.join(", ") || "-"}</span>
<span className="pill">{row.presence}</span>
<span>{row.interfaces.join(", ") || "-"}</span>
<span>
<span className="device-cell" data-label="Name" title={row.name}>{row.name}</span>
<span className="device-cell muted" data-label="IP" title={row.ips.join(", ") || "-"}>{summarize(row.ips)}</span>
<span className="device-cell muted" data-label="MAC" title={row.macs.join(", ") || "-"}>{summarize(row.macs)}</span>
<span className="device-cell" data-label="Presence"><span className="pill">{row.presence}</span></span>
<span className="device-cell" data-label="Interfaces" title={row.interfaces.join(", ") || "-"}>{summarize(row.interfaces)}</span>
<span className="device-cell device-action" data-label="">
<button onClick={() => void wakeDevice(row)} disabled={wakeBusyId === row.id || !selectedAgentId}>
{wakeBusyId === row.id ? "Waking..." : "Wake"}
</button>
+79 -21
View File
@@ -168,33 +168,91 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
}
.device-row {
align-items: stretch;
}
.device-main {
min-width: 0;
}
.device-main strong {
display: inline-block;
margin-bottom: 0.25rem;
}
.device-meta {
display: flex;
display: grid;
grid-template-columns: minmax(110px, 1.1fr) minmax(160px, 2fr) minmax(130px, 1.5fr) minmax(110px, 0.9fr) minmax(120px, 1fr) auto;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.55rem;
}
.device-header {
font-weight: 600;
position: sticky;
top: 0;
z-index: 2;
background: #091429;
}
.device-cell {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sortable-col {
cursor: pointer;
user-select: none;
}
.device-action {
justify-self: end;
}
@media (max-width: 720px) {
.device-row {
flex-direction: column;
.device-header {
display: none;
}
.device-meta {
justify-content: flex-start;
.device-row {
grid-template-columns: 1fr auto;
grid-template-areas:
"name action"
"presence presence"
"ip ip"
"mac mac"
"interfaces interfaces";
align-items: start;
}
.device-row .device-cell {
white-space: normal;
overflow-wrap: anywhere;
text-overflow: clip;
}
.device-row .device-cell[data-label]::before {
content: attr(data-label);
display: block;
font-size: 0.72rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.03em;
margin-bottom: 0.1rem;
}
.device-row .device-cell[data-label="Name"] {
grid-area: name;
}
.device-row .device-cell[data-label="Presence"] {
grid-area: presence;
}
.device-row .device-cell[data-label="IP"] {
grid-area: ip;
}
.device-row .device-cell[data-label="MAC"] {
grid-area: mac;
}
.device-row .device-cell[data-label="Interfaces"] {
grid-area: interfaces;
}
.device-row .device-action {
grid-area: action;
justify-self: end;
}
.quick-wake {