misc changes

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