misc changes
This commit is contained in:
@@ -12,7 +12,7 @@ start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command "$BIN" serve --config "$CONFIG"
|
||||
procd_set_param file "$CONFIG"
|
||||
procd_set_param env RUST_LOG=wakey_agent=debug,wakey=debug
|
||||
# procd_set_param env RUST_LOG=wakey_agent=debug,wakey=debug
|
||||
procd_set_param respawn 5 1 0
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
+20
-19
@@ -77,7 +77,7 @@ pub fn merge_devices_with_observations(
|
||||
let key = row
|
||||
.mac
|
||||
.map(|m| m.to_string())
|
||||
.unwrap_or_else(|| format!("ip:{}", row.ip));
|
||||
.unwrap_or_else(|| format!("ip:{}", row.ip)); // key by mac or by FAILED ip
|
||||
by_key.entry(key).or_default().0.push(row);
|
||||
}
|
||||
for lease in leases {
|
||||
@@ -99,30 +99,31 @@ pub fn merge_devices_with_observations(
|
||||
Device::from_parts_with_observations(neighbors, leases, observations)
|
||||
})
|
||||
.collect();
|
||||
if !query.is_empty() {
|
||||
let mut texts = Vec::new();
|
||||
let mut devs = Vec::new();
|
||||
let mut ips = Vec::new();
|
||||
let mut macs = Vec::new();
|
||||
let mut nuds = Vec::new();
|
||||
|
||||
let mut texts = Vec::new();
|
||||
let mut devs = Vec::new();
|
||||
let mut ips = Vec::new();
|
||||
let mut macs = Vec::new();
|
||||
let mut nuds = Vec::new();
|
||||
for term in query {
|
||||
match term {
|
||||
Query::Text(v) => texts.push(v.as_str()),
|
||||
Query::Interface(v) => devs.push(v.as_str()),
|
||||
Query::Ip(v) => ips.push(*v),
|
||||
Query::Mac(v) => macs.push(*v),
|
||||
Query::NeighborState(v) => nuds.push(*v),
|
||||
};
|
||||
}
|
||||
|
||||
for term in query {
|
||||
match term {
|
||||
Query::Text(v) => texts.push(v.as_str()),
|
||||
Query::Interface(v) => devs.push(v.as_str()),
|
||||
Query::Ip(v) => ips.push(*v),
|
||||
Query::Mac(v) => macs.push(*v),
|
||||
Query::NeighborState(v) => nuds.push(*v),
|
||||
};
|
||||
}
|
||||
|
||||
devices.retain(|device| {
|
||||
(texts.is_empty() || device.names.iter().any(|n| texts.contains(&n.as_str())))
|
||||
devices.retain(|device| {
|
||||
(texts.is_empty() || device.names.iter().any(|n| texts.contains(&n.as_str())))
|
||||
&& (devs.is_empty() || device.interfaces.iter().any(|i| devs.contains(&i.as_str()))) // same pattern as iter().any
|
||||
&& (ips.is_empty() || device.ips.iter().any(|ip| ips.contains(ip)))
|
||||
&& (macs.is_empty() || device.macs.iter().any(|mac| macs.contains(mac)))
|
||||
&& (nuds.is_empty() || device.neighbors.iter().any(|n| nuds.contains(&n.state)))
|
||||
});
|
||||
});
|
||||
}
|
||||
devices.sort_by(|a, b| {
|
||||
presence_rank(b.presence)
|
||||
.cmp(&presence_rank(a.presence))
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wakey Operator UI</title>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = localStorage.getItem("wakey-ui-theme");
|
||||
if (
|
||||
t === "dark" ||
|
||||
((t === "system" || !t) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -144,6 +144,7 @@ export function App() {
|
||||
selectedAgentId={selectedAgentId}
|
||||
onSelectAgent={setSelectedAgentId}
|
||||
onAfterWake={loadAll}
|
||||
onRefresh={loadAll}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function AppLayout() {
|
||||
className={[
|
||||
"sidebar",
|
||||
collapsed ? "sidebar--collapsed" : "",
|
||||
"fixed inset-y-0 left-0 z-50 shadow-xl transition-all duration-300 ease-in-out",
|
||||
"fixed inset-y-0 left-0 z-50 shadow-xl transition-all duration-100 ease-in-out",
|
||||
"md:sticky md:top-0 md:z-30 md:shadow-none",
|
||||
collapsed ? "-translate-x-full md:translate-x-0" : "translate-x-0",
|
||||
]
|
||||
@@ -219,8 +219,14 @@ export function AppLayout() {
|
||||
{/* Mobile backdrop */}
|
||||
{!collapsed && (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="fixed inset-0 z-40 bg-black/20 md:hidden animate-in fade-in duration-200"
|
||||
onClick={() => setCollapsed(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") setCollapsed(true);
|
||||
}}
|
||||
aria-label="Close menu"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="alert-severity" className="grid gap-1 text-sm text-muted-foreground">
|
||||
Severity
|
||||
<Select
|
||||
value={severity}
|
||||
@@ -80,7 +80,7 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
if (value) setSeverity(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger id="alert-severity" className="w-full">
|
||||
<SelectValue placeholder="Severity" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -93,7 +93,7 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="alert-status" className="grid gap-1 text-sm text-muted-foreground">
|
||||
Status
|
||||
<Select
|
||||
value={status}
|
||||
@@ -101,7 +101,7 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
if (value) setStatus(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger id="alert-status" className="w-full">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -114,7 +114,7 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="alert-kind" className="grid gap-1 text-sm text-muted-foreground">
|
||||
Kind
|
||||
<Select
|
||||
value={kind}
|
||||
@@ -122,7 +122,7 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
if (value) setKind(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger id="alert-kind" className="w-full">
|
||||
<SelectValue placeholder="Kind" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -170,9 +170,10 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="alert-transition-search" className="grid gap-1 text-sm text-muted-foreground">
|
||||
Search
|
||||
<Input
|
||||
id="alert-transition-search"
|
||||
value={transitionQ}
|
||||
onChange={(e) => setTransitionQ(e.target.value)}
|
||||
placeholder="kind, status, message, agent"
|
||||
|
||||
@@ -70,7 +70,7 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="audit-event-type" className="grid gap-1 text-sm text-muted-foreground">
|
||||
Event type
|
||||
<Select
|
||||
value={eventType}
|
||||
@@ -78,7 +78,7 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
if (value) setEventType(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger id="audit-event-type" className="w-full">
|
||||
<SelectValue placeholder="Event type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -91,7 +91,7 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="audit-outcome" className="grid gap-1 text-sm text-muted-foreground">
|
||||
Outcome
|
||||
<Select
|
||||
value={outcome}
|
||||
@@ -99,7 +99,7 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
if (value) setOutcome(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger id="audit-outcome" className="w-full">
|
||||
<SelectValue placeholder="Outcome" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -112,9 +112,10 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="audit-search" className="grid gap-1 text-sm text-muted-foreground">
|
||||
Search
|
||||
<Input
|
||||
id="audit-search"
|
||||
value={needle}
|
||||
onChange={(e) => setNeedle(e.target.value)}
|
||||
placeholder="message, agent, actor"
|
||||
|
||||
@@ -66,12 +66,12 @@ export function CommandsPage({
|
||||
Command Runner
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Debug tool — send raw commands to agents and inspect JSON responses
|
||||
Debug tool: send raw commands to agents and inspect JSON responses
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="grid gap-3" onSubmit={submit}>
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="cmd-agent" className="grid gap-1 text-sm text-muted-foreground">
|
||||
<span>Agent</span>
|
||||
<AgentSelector
|
||||
agents={agents}
|
||||
@@ -80,13 +80,13 @@ export function CommandsPage({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="cmd-kind" className="grid gap-1 text-sm text-muted-foreground">
|
||||
<span>Command</span>
|
||||
<Select
|
||||
value={kind}
|
||||
onValueChange={(value) => setKind(value as CommandKind)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger id="cmd-kind" className="w-full">
|
||||
<SelectValue placeholder="Pick command" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -98,9 +98,10 @@ export function CommandsPage({
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="cmd-query" className="grid gap-1 text-sm text-muted-foreground">
|
||||
<span>Query</span>
|
||||
<Input
|
||||
id="cmd-query"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="optional"
|
||||
|
||||
@@ -115,7 +115,7 @@ export function DashboardPage({
|
||||
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{/* Fleet Status */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-y-0 pb-2">
|
||||
<CardDescription className="flex items-center gap-1.5">
|
||||
<Bot className="size-3.5" />
|
||||
Fleet Status
|
||||
@@ -152,7 +152,7 @@ export function DashboardPage({
|
||||
|
||||
{/* Active Alerts */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-y-0 pb-2">
|
||||
<CardDescription className="flex items-center gap-1.5">
|
||||
<Bell className="size-3.5" />
|
||||
Active Alerts
|
||||
@@ -188,7 +188,7 @@ export function DashboardPage({
|
||||
|
||||
{/* Transitions */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-y-0 pb-2">
|
||||
<CardDescription className="flex items-center gap-1.5">
|
||||
<Activity className="size-3.5" />
|
||||
Transitions
|
||||
@@ -206,7 +206,7 @@ export function DashboardPage({
|
||||
|
||||
{/* Token Health */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-y-0 pb-2">
|
||||
<CardDescription className="flex items-center gap-1.5">
|
||||
<Key className="size-3.5" />
|
||||
Enroll Tokens
|
||||
@@ -228,7 +228,7 @@ export function DashboardPage({
|
||||
|
||||
{/* Recent Activity Feed */}
|
||||
<Card className="sm:col-span-2 lg:col-span-4">
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3 space-y-0">
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3 gap-y-0">
|
||||
<div>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
|
||||
@@ -51,9 +51,10 @@ type Props = {
|
||||
selectedAgentId: string;
|
||||
onSelectAgent: (agentId: string) => void;
|
||||
onAfterWake?: () => Promise<void>;
|
||||
onRefresh?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function DevicesPage({ agents, onAfterWake }: Props) {
|
||||
export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) {
|
||||
const [devices, setDevices] = useState<FleetDevice[]>([]);
|
||||
const [knownDevices, setKnownDevices] = useState<KnownDevice[]>([]);
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -137,8 +138,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
|
||||
} catch {
|
||||
const area = document.createElement("textarea");
|
||||
area.value = text;
|
||||
area.style.position = "fixed";
|
||||
area.style.opacity = "0";
|
||||
area.style.cssText = "position:fixed;opacity:0";
|
||||
document.body.appendChild(area);
|
||||
area.select();
|
||||
document.execCommand("copy");
|
||||
@@ -201,7 +201,10 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void loadFleet()}
|
||||
onClick={() => {
|
||||
void loadFleet();
|
||||
if (onRefresh) void onRefresh();
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className="size-4" aria-hidden />
|
||||
@@ -222,11 +225,12 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid gap-2 lg:grid-cols-[minmax(16rem,1fr)_11rem_10rem_14rem_10rem]">
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="fleet-search" className="grid gap-1 text-sm text-muted-foreground">
|
||||
<span>Search</span>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="fleet-search"
|
||||
className="pl-8"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
@@ -246,7 +250,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
|
||||
values={knownFilters}
|
||||
onChange={(value) => setKnown(value as KnownFilter)}
|
||||
/>
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="fleet-agent" className="grid gap-1 text-sm text-muted-foreground">
|
||||
<span>Agent</span>
|
||||
<Select
|
||||
value={agentId}
|
||||
@@ -334,6 +338,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
|
||||
</Card>
|
||||
|
||||
<FleetDeviceDetailsDialog
|
||||
key={details?.device_key ?? "__none"}
|
||||
device={details}
|
||||
knownDevices={knownDevices}
|
||||
open={Boolean(details)}
|
||||
|
||||
@@ -118,7 +118,7 @@ export function TokensPage() {
|
||||
return (
|
||||
<section className="grid gap-4 xl:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3 space-y-0">
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3 gap-y-0">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Key className="size-5 text-primary" />
|
||||
@@ -219,9 +219,10 @@ export function TokensPage() {
|
||||
</Button>
|
||||
</div>
|
||||
{useCustom && (
|
||||
<label className="mt-2 grid gap-1 text-sm text-muted-foreground">
|
||||
<label htmlFor="token-custom-ttl" className="mt-2 grid gap-1 text-sm text-muted-foreground">
|
||||
<span>TTL (seconds)</span>
|
||||
<Input
|
||||
id="token-custom-ttl"
|
||||
type="number"
|
||||
min={1}
|
||||
value={customTtl}
|
||||
|
||||
@@ -101,7 +101,7 @@ export function WakeToolsPage({
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<label className="grid gap-1.5 text-sm text-muted-foreground">
|
||||
<label htmlFor="wake-agent" className="grid gap-1.5 text-sm text-muted-foreground">
|
||||
<span>Agent</span>
|
||||
<AgentSelector
|
||||
agents={agents}
|
||||
@@ -111,14 +111,14 @@ export function WakeToolsPage({
|
||||
</label>
|
||||
|
||||
<form onSubmit={handleWake} className="grid gap-3">
|
||||
<label className="grid gap-1.5 text-sm text-muted-foreground">
|
||||
<label htmlFor="wake-target" className="grid gap-1.5 text-sm text-muted-foreground">
|
||||
<span>Target</span>
|
||||
<Input
|
||||
id="wake-target"
|
||||
value={target}
|
||||
onChange={(e) => setTarget(e.target.value)}
|
||||
placeholder="bedroom-pc, 192.168.1.100, or aa:bb:cc:dd:ee:ff"
|
||||
disabled={busy}
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
@@ -134,7 +134,7 @@ export function WakeToolsPage({
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-2 gap-y-0">
|
||||
<CardTitle>Recent Wakes</CardTitle>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -147,10 +147,10 @@ export function WakeToolsPage({
|
||||
<CardContent>
|
||||
{recentWakes.length > 0 ? (
|
||||
<div className="grid gap-0">
|
||||
{recentWakes.map((event, idx) => (
|
||||
{recentWakes.map((event, i) => (
|
||||
<div
|
||||
className={`flex items-start justify-between gap-3 py-2.5 ${idx < recentWakes.length - 1 ? "border-b border-border/50" : ""}`}
|
||||
key={`${event.ts}-${idx}`}
|
||||
className={`flex items-start justify-between gap-3 py-2.5 ${i < recentWakes.length - 1 ? "border-b border-border/50" : ""}`}
|
||||
key={`${event.ts}-${event.target}-${event.agentId}`}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Minus, Plus, Zap } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -52,24 +52,14 @@ export function FleetDeviceDetailsDialog({
|
||||
onCopy,
|
||||
onChanged,
|
||||
}: Props) {
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [displayName, setDisplayName] = useState(device?.display_name ?? "");
|
||||
const [targetDeviceId, setTargetDeviceId] = useState("");
|
||||
const [routeId, setRouteId] = useState("");
|
||||
const [routeId, setRouteId] = useState(device?.recommended_route?.route_id ?? "");
|
||||
const [addKind, setAddKind] = useState<"mac" | "ip">("mac");
|
||||
const [addValue, setAddValue] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(device?.display_name ?? "");
|
||||
setTargetDeviceId("");
|
||||
setRouteId(device?.recommended_route?.route_id ?? "");
|
||||
setAddKind("mac");
|
||||
setAddValue("");
|
||||
setError("");
|
||||
setActionBusy(false);
|
||||
}, [device]);
|
||||
|
||||
const rowIdentifiers = device ? identifiersFor(device) : [];
|
||||
|
||||
// Resolve the full KnownDevice object if this fleet row is known
|
||||
@@ -279,7 +269,7 @@ export function FleetDeviceDetailsDialog({
|
||||
<span className="font-medium">{fullKnown.display_name}</span>
|
||||
{fullKnown.notes && (
|
||||
<span className="text-muted-foreground">
|
||||
— {fullKnown.notes}
|
||||
: {fullKnown.notes}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -135,7 +135,7 @@
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground transition-colors duration-100 ease-in-out;
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -189,6 +189,7 @@
|
||||
background-size: 100% 100%;
|
||||
overflow-y: auto;
|
||||
max-width: 100%;
|
||||
transition: background 100ms ease-in-out;
|
||||
}
|
||||
|
||||
/* ── Sidebar internals ────────────────────────────────────────────────── */
|
||||
|
||||
@@ -261,6 +261,7 @@ fn add_agent_device_to_entry(
|
||||
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref(), "device");
|
||||
let wakeable = status.connected && !device_offline;
|
||||
entry.routes.insert(
|
||||
// one per mac... when is one per mac/ip pair? this a regression.
|
||||
rid.clone(),
|
||||
FleetWakeRoute {
|
||||
route_id: rid,
|
||||
|
||||
@@ -85,12 +85,9 @@ pub async fn refresh_fleet_devices(
|
||||
});
|
||||
continue;
|
||||
};
|
||||
match serde_json::from_value::<Vec<wakey_core::Device>>(
|
||||
result
|
||||
.get("devices")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
) {
|
||||
match serde_json::from_value::<wakey_core::DeviceInventory>(result)
|
||||
.map(|i| i.devices)
|
||||
{
|
||||
Ok(devices) => {
|
||||
match state
|
||||
.store
|
||||
@@ -276,6 +273,7 @@ async fn load_fleet_devices(
|
||||
let mut devices = build_fleet_devices(known_devices, agent_devices, &context);
|
||||
filter_fleet_devices(&mut devices, query);
|
||||
let limit = query.limit.unwrap_or(500).clamp(1, 1000);
|
||||
// this is going to be 10 million times harder if i decide to do this lazily
|
||||
devices.truncate(limit);
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@ pub enum DeviceId {
|
||||
/// One raw source fact used while building a device aggregate.
|
||||
///
|
||||
/// These are intentionally source-shaped and non-durable. They preserve details
|
||||
/// from hooks and live inventory so higher layers can explain why a device looks
|
||||
/// online, stale, or unknown without reverse-engineering flattened fields.
|
||||
/// from hooks (arp, dhcp, etc) so higher layers can explain why a device looks
|
||||
/// online, stale, or unknown.
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||
pub struct DeviceObservationFact {
|
||||
@@ -145,13 +145,14 @@ impl Device {
|
||||
}
|
||||
presence = std::cmp::max(presence, Presence::from(neighbor.state));
|
||||
}
|
||||
|
||||
let mut observed_non_remove = false;
|
||||
for observation in &observations {
|
||||
if let Some(name) = observation.hostname.as_deref() {
|
||||
names.insert(name);
|
||||
}
|
||||
if let Some(ip) = observation.ip {
|
||||
ips.insert(ip);
|
||||
ips.insert(ip); // should we just add removed IPs? Or a separate map of presence -> IP sets?
|
||||
}
|
||||
if let Some(mac) = observation.mac {
|
||||
macs.insert(mac);
|
||||
@@ -196,11 +197,13 @@ fn observation_presence(observation: &DeviceObservationFact) -> Presence {
|
||||
(_, "remove") => Presence::Offline,
|
||||
("neigh", "add" | "update" | "old") => Presence::LikelyOnline,
|
||||
_ => Presence::Unknown,
|
||||
// dhcp events does not say anything about offline status, but this may shadow neighbor entries. This may be undesirable.
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of merged discovered devices.
|
||||
#[derive(Debug, Default, Clone, Serialize)]
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceInventory {
|
||||
#[serde(default)]
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
|
||||
@@ -235,11 +235,11 @@ pub async fn observe_dhcp_event(
|
||||
row.hostname = hostname.clone();
|
||||
row.last_action = action.to_string();
|
||||
row.last_seen_unix = now;
|
||||
changed = true;
|
||||
changed = true; // update row
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
changed = true;
|
||||
changed = true; // insert new row
|
||||
ObservedDhcpClient {
|
||||
mac: mac_s.clone(),
|
||||
ip,
|
||||
@@ -258,7 +258,7 @@ pub async fn observe_dhcp_event(
|
||||
if cache.get(&mac_s).map(|v| v != &hostname).unwrap_or(true) {
|
||||
cache.insert(mac_s, hostname);
|
||||
save_mac_name_cache(&cache).await?;
|
||||
changed = true;
|
||||
changed = true; // update mac -> name cache...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ pub async fn observe_neighbor_event(
|
||||
.find_map(|alias| store.neighbors.remove(alias))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
changed = true;
|
||||
changed = true; // append
|
||||
ObservedNeighbor {
|
||||
key: key.clone(),
|
||||
mac: mac.clone(),
|
||||
@@ -304,7 +304,7 @@ pub async fn observe_neighbor_event(
|
||||
});
|
||||
for alias in alias_keys {
|
||||
if store.neighbors.remove(&alias).is_some() {
|
||||
changed = true;
|
||||
changed = true; // remove old alias
|
||||
}
|
||||
}
|
||||
if row.key != key
|
||||
@@ -318,7 +318,7 @@ pub async fn observe_neighbor_event(
|
||||
row.ip = ip;
|
||||
row.last_action = action.clone();
|
||||
row.last_seen_unix = now;
|
||||
changed = true;
|
||||
changed = true; // something changed
|
||||
}
|
||||
store.neighbors.insert(key, row);
|
||||
if let (Some(mac), Some(ip)) = (mac.as_deref(), ip)
|
||||
@@ -387,6 +387,7 @@ fn mark_replaced_neighbor_ips_removed(
|
||||
let Some(row_ip) = row.ip else {
|
||||
continue;
|
||||
};
|
||||
// found another ip for same mac, mark it as removed
|
||||
if row.mac.as_deref() == Some(mac)
|
||||
&& row_ip != current_ip
|
||||
&& same_ip_family(row_ip, current_ip)
|
||||
@@ -412,7 +413,7 @@ mod tests {
|
||||
use serial_test::serial;
|
||||
|
||||
struct EnvGuard {
|
||||
keys: Vec<&'static str>,
|
||||
keys: Vec<&'static str>, // vec of 1 key
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
|
||||
Reference in New Issue
Block a user