This commit is contained in:
lda
2026-04-12 18:28:29 +07:00 Unverified
parent c8a986148d
commit a57535c6a2
3 changed files with 104 additions and 35 deletions
+9 -3
View File
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import type { AuditEvent } from "@/api"; import type { AuditEvent } from "@/api";
@@ -8,9 +9,14 @@ type Props = {
}; };
export function AuditPage({ events, onRefresh }: Props) { export function AuditPage({ events, onRefresh }: Props) {
const [eventType, setEventType] = useState("all"); const [searchParams] = useSearchParams();
const [outcome, setOutcome] = useState("all"); const initialEventType = searchParams.get("event_type") || "all";
const [needle, setNeedle] = useState(""); const initialOutcome = searchParams.get("outcome") || "all";
const initialNeedle = searchParams.get("q") || "";
const [eventType, setEventType] = useState(initialEventType);
const [outcome, setOutcome] = useState(initialOutcome);
const [needle, setNeedle] = useState(initialNeedle);
const eventTypes = useMemo( const eventTypes = useMemo(
() => ["all", ...Array.from(new Set(events.map((e) => e.event_type))).sort()], () => ["all", ...Array.from(new Set(events.map((e) => e.event_type))).sort()],
+70 -32
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { runCommand, type Agent } from "@/api"; import { runCommand, type Agent } from "@/api";
@@ -80,16 +81,25 @@ function parseWakeSummary(response: unknown): { outcome: string; requestId: stri
} }
const entries = Array.isArray(result.result) ? result.result : []; const entries = Array.isArray(result.result) ? result.result : [];
const statuses = entries const targetOutcomes = entries
.map((row) => { .map((row) => {
if (!row || typeof row !== "object") return ""; if (!row || typeof row !== "object") return "";
const statusRow = (row as Record<string, unknown>).status as Record<string, unknown> | undefined; const rowObj = row as Record<string, unknown>;
return typeof statusRow?.kind === "string" ? statusRow.kind : ""; const statusRaw = rowObj.status;
const status =
typeof statusRaw === "string"
? statusRaw
: (typeof (statusRaw as Record<string, unknown> | undefined)?.kind === "string"
? String((statusRaw as Record<string, unknown>).kind)
: "unknown");
const ip = typeof rowObj.ip === "string" ? rowObj.ip : "?";
const mac = typeof rowObj.mac === "string" ? rowObj.mac : "?";
return `${status}(${ip}/${mac})`;
}) })
.filter(Boolean); .filter(Boolean);
const detail = statuses.length ? statuses.join(", ") : "wake dispatched"; const detail = targetOutcomes.length ? targetOutcomes.join(", ") : "wake dispatched";
const failed = statuses.some((s) => s === "error" || s === "incomplete"); const failed = targetOutcomes.some((s) => s.startsWith("incomplete") || s.startsWith("nonexistent_address") || s.startsWith("wrong_size"));
return { outcome: failed ? "error" : "ok", requestId, detail }; return { outcome: failed ? "error" : "ok", requestId, detail };
} }
@@ -132,7 +142,16 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [wakeBusyId, setWakeBusyId] = useState(""); const [wakeBusyId, setWakeBusyId] = useState("");
const [quickWake, setQuickWake] = useState("");
const [recentWakes, setRecentWakes] = useState<WakeEvent[]>([]); const [recentWakes, setRecentWakes] = useState<WakeEvent[]>([]);
function appendWakeEvent(event: WakeEvent) {
setRecentWakes((prev) => {
const next = [event, ...prev].slice(0, 20);
saveHistory(next);
return next;
});
}
useEffect(() => { useEffect(() => {
setRecentWakes(loadHistory()); setRecentWakes(loadHistory());
@@ -166,40 +185,34 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
async function wakeDevice(device: DeviceRow) { async function wakeDevice(device: DeviceRow) {
const target = chooseWakeTarget(device); const target = chooseWakeTarget(device);
await wakeTarget(target, device.id);
}
async function wakeTarget(target: string, busyId: string) {
if (!target || !selectedAgentId) return; if (!target || !selectedAgentId) return;
setWakeBusyId(device.id); setWakeBusyId(busyId);
try { try {
const response = await runCommand(selectedAgentId, "wake", target); const response = await runCommand(selectedAgentId, "wake", target);
const summary = parseWakeSummary(response); const summary = parseWakeSummary(response);
const next: WakeEvent[] = [ appendWakeEvent({
{ ts: Date.now(),
ts: Date.now(), target,
target, agentId: selectedAgentId,
agentId: selectedAgentId, outcome: summary.outcome,
outcome: summary.outcome, requestId: summary.requestId,
requestId: summary.requestId, detail: summary.detail,
detail: summary.detail, });
},
...recentWakes,
].slice(0, 20);
setRecentWakes(next);
saveHistory(next);
await loadInventory(); await loadInventory();
if (onAfterWake) await onAfterWake(); if (onAfterWake) await onAfterWake();
} catch (err) { } catch (err) {
const next: WakeEvent[] = [ appendWakeEvent({
{ ts: Date.now(),
ts: Date.now(), target,
target, agentId: selectedAgentId,
agentId: selectedAgentId, outcome: "error",
outcome: "error", requestId: "",
requestId: "", detail: String(err),
detail: String(err), });
},
...recentWakes,
].slice(0, 20);
setRecentWakes(next);
saveHistory(next);
} finally { } finally {
setWakeBusyId(""); setWakeBusyId("");
} }
@@ -257,6 +270,23 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
</label> </label>
</div> </div>
<div className="quick-wake">
<label>
Quick Wake (name, IP, or MAC)
<input
value={quickWake}
onChange={(e) => setQuickWake(e.target.value)}
placeholder="bedroom-pc or aa:bb:cc:dd:ee:ff"
/>
</label>
<button
onClick={() => void wakeTarget(quickWake.trim(), "quick")}
disabled={!selectedAgentId || !quickWake.trim() || wakeBusyId === "quick"}
>
{wakeBusyId === "quick" ? "Waking..." : "Wake target"}
</button>
</div>
<p className="muted">Showing {filtered.length} of {rows.length}</p> <p className="muted">Showing {filtered.length} of {rows.length}</p>
{error && <pre className="error">{error}</pre>} {error && <pre className="error">{error}</pre>}
<div className="list device-list"> <div className="list device-list">
@@ -299,6 +329,14 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
<strong>{event.target}</strong> <strong>{event.target}</strong>
<div className="muted">{new Date(event.ts).toLocaleString()} on {event.agentId}</div> <div className="muted">{new Date(event.ts).toLocaleString()} on {event.agentId}</div>
<div className="muted">{event.detail}</div> <div className="muted">{event.detail}</div>
{event.requestId && (
<Link
className="muted linkish"
to={`/audit?q=${encodeURIComponent(event.requestId)}`}
>
View related audit ({event.requestId})
</Link>
)}
</div> </div>
<span className={`pill ${event.outcome === "ok" ? "ready" : "error"}`}> <span className={`pill ${event.outcome === "ok" ? "ready" : "error"}`}>
{event.outcome} {event.outcome}
+25
View File
@@ -123,6 +123,27 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
font-size: 0.9rem; font-size: 0.9rem;
} }
.quick-wake {
display: grid;
grid-template-columns: 1fr auto;
gap: 0.6rem;
align-items: end;
margin-top: 0.65rem;
}
.quick-wake label {
display: grid;
gap: 0.35rem;
color: var(--muted);
font-size: 0.9rem;
}
.linkish {
display: inline-block;
margin-top: 0.25rem;
text-decoration: underline;
}
.list { display: grid; gap: 0.4rem; margin-top: 0.55rem; } .list { display: grid; gap: 0.4rem; margin-top: 0.55rem; }
.row { .row {
display: flex; display: flex;
@@ -175,6 +196,10 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
.device-meta { .device-meta {
justify-content: flex-start; justify-content: flex-start;
} }
.quick-wake {
grid-template-columns: 1fr;
}
} }
.empty { color: var(--muted); padding: 0.35rem 0.2rem; } .empty { color: var(--muted); padding: 0.35rem 0.2rem; }