stuff
This commit is contained in:
@@ -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()],
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user