Compare commits
54
Commits
v0.2.1
...
8628af6bd3
No files matched your search
@@ -16,7 +16,14 @@ cw = "c --target=x86_64-pc-windows-msvc"
|
|||||||
bw = "b --target=x86_64-pc-windows-msvc"
|
bw = "b --target=x86_64-pc-windows-msvc"
|
||||||
tw = "t --target=x86_64-pc-windows-msvc"
|
tw = "t --target=x86_64-pc-windows-msvc"
|
||||||
|
|
||||||
|
|
||||||
|
check-all = "check --workspace --all-features --all-targets"
|
||||||
|
clippy-all = "clippy --workspace --all-targets --all-features" # -- -D warnings
|
||||||
|
test-all = "test --workspace --all-features --all-targets"
|
||||||
|
fmt-all = "fmt --all"
|
||||||
|
|
||||||
# idk below
|
# idk below
|
||||||
|
ldar = "r -qrp"
|
||||||
ldabr = "b -r --target=armv7-unknown-linux-musleabihf"
|
ldabr = "b -r --target=armv7-unknown-linux-musleabihf"
|
||||||
t = "test -- --nocapture --test-threads=1"
|
t = "test -- --nocapture --test-threads=1"
|
||||||
tdebug = "test -- --nocapture --test-threads=1 --show-output"
|
tdebug = "test -- --nocapture --test-threads=1 --show-output"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
SQLX_OFFLINE=true
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# Wakey Checkpoint (2026-04-11)
|
|
||||||
|
|
||||||
## Snapshot
|
|
||||||
|
|
||||||
This checkpoint captures the current state after control-plane migration, logging hardening, config ergonomics, and state storage upgrades.
|
|
||||||
|
|
||||||
## What Is Done
|
|
||||||
|
|
||||||
- Legacy router-hosted HTTP/static layer removed from `wakey` crate.
|
|
||||||
- New `wakey-control-plane` crate is active for:
|
|
||||||
- enroll-token issuance
|
|
||||||
- agent enrollment
|
|
||||||
- connected-agent websocket registry
|
|
||||||
- command relay to agent
|
|
||||||
- `wakey-agent` is active for outbound websocket execution and local command dispatch.
|
|
||||||
|
|
||||||
## Logging + Telemetry
|
|
||||||
|
|
||||||
- High-signal logs were added across:
|
|
||||||
- control-plane API relay path
|
|
||||||
- control-plane websocket lifecycle
|
|
||||||
- control-plane state lifecycle
|
|
||||||
- agent command lifecycle, session lifecycle, and enrollment
|
|
||||||
- Correlated relay spans include command context (`agent_id`, `request_id`, `command`).
|
|
||||||
- Control-plane telemetry is config-driven:
|
|
||||||
- optional OTLP endpoint
|
|
||||||
- optional JSON logs
|
|
||||||
- fallback local logs when OTLP endpoint is not set
|
|
||||||
|
|
||||||
## Config Ergonomics
|
|
||||||
|
|
||||||
### Control-plane
|
|
||||||
|
|
||||||
- Config file support is wired (`/etc/wakey-control-plane/config.toml` by default).
|
|
||||||
- `serve` can read defaults from config file and CLI can override.
|
|
||||||
- New `init-config` command scaffolds a control-plane config file.
|
|
||||||
|
|
||||||
### Agent
|
|
||||||
|
|
||||||
- Existing `init-config` command scaffolds agent config.
|
|
||||||
- Enrollment can optionally signal reload of running daemon.
|
|
||||||
|
|
||||||
## State Storage Upgrade
|
|
||||||
|
|
||||||
- Control-plane state backend moved from JSON snapshot to embedded `sled` DB.
|
|
||||||
- Default state path changed to `/var/lib/wakey-control-plane/state.db`.
|
|
||||||
- Legacy JSON migration support has been removed; sled is now the only supported
|
|
||||||
state format.
|
|
||||||
- Enroll tokens include persisted expiry timestamps and are validated on enroll.
|
|
||||||
- Periodic and explicit garbage collection remove expired tokens.
|
|
||||||
|
|
||||||
## Operator Commands
|
|
||||||
|
|
||||||
### Control-plane bootstrap
|
|
||||||
|
|
||||||
```sh
|
|
||||||
wakey-control-plane init-config
|
|
||||||
wakey-control-plane serve --config-file /etc/wakey-control-plane/config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue enroll token (live daemon path)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
wakey-control-plane issue-enroll-token --public-url https://cp.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Agent bootstrap
|
|
||||||
|
|
||||||
```sh
|
|
||||||
wakey-agent enroll --server-url https://cp.example.com --enroll-token <token>
|
|
||||||
wakey-agent serve --config /etc/wakey-agent/config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Health
|
|
||||||
|
|
||||||
- Last verified passing:
|
|
||||||
- `cargo check --workspace`
|
|
||||||
- `cargo clippy --workspace`
|
|
||||||
|
|
||||||
## Test Coverage Added In This Pass
|
|
||||||
|
|
||||||
- Unit tests in `wakey-control-plane/src/state/store.rs` now cover:
|
|
||||||
- expired-token garbage collection removes persisted stale tokens
|
|
||||||
- enroll rejects expired tokens and consumes stale entries
|
|
||||||
- state stats counters for agents and expired token totals
|
|
||||||
|
|
||||||
## Known Tradeoffs / Follow-ups
|
|
||||||
|
|
||||||
- Reload semantics with `sled` are now mostly no-op for in-memory state (data is durable in DB).
|
|
||||||
- No dedicated state-inspection CLI command yet (suggestion: add `state-stats` command).
|
|
||||||
- OTLP configuration is currently control-plane focused; agent parity can be added if needed.
|
|
||||||
|
|
||||||
## Suggested Next Steps
|
|
||||||
|
|
||||||
1. Add a control-plane `state-stats` command to print DB path, agent count, token count.
|
|
||||||
2. Add symmetric telemetry config support in `wakey-agent` config file.
|
|
||||||
3. Add integration tests for:
|
|
||||||
- enroll + relay over websocket
|
|
||||||
- legacy JSON-to-sled migration path
|
|
||||||
- init-config command behavior and overrides
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# Wakey Checkpoint (2026-04-12)
|
|
||||||
|
|
||||||
## Snapshot
|
|
||||||
|
|
||||||
This checkpoint captures progress after adding audit persistence, alert evaluation and transitions, control-plane alert APIs, and websocket timing diagnostics across agent and control-plane.
|
|
||||||
|
|
||||||
## Major Changes Landed
|
|
||||||
|
|
||||||
- Control-plane audit system implemented with persistent sled-backed events.
|
|
||||||
- Audit emission wired into:
|
|
||||||
- enroll accept/reject
|
|
||||||
- token issue/list/revoke
|
|
||||||
- command dispatch/result/error/timeout
|
|
||||||
- websocket auth accept/reject and disconnect
|
|
||||||
- Audit query API added:
|
|
||||||
- `GET /api/v1/control/audit/events`
|
|
||||||
- Active alert engine added with deterministic rules over audit + live session state.
|
|
||||||
- Alert APIs added:
|
|
||||||
- `GET /api/v1/control/alerts`
|
|
||||||
- `GET /api/v1/control/alerts/history`
|
|
||||||
- `GET /api/v1/control/alerts/ws`
|
|
||||||
- Alert transition persistence added (open/resolve transitions tracked across evaluations).
|
|
||||||
- Route classes split explicitly in runtime:
|
|
||||||
- public routes (enroll/ws/health)
|
|
||||||
- control routes (`/api/v1/control/*`)
|
|
||||||
- Caddy template added for edge policy and Cloudflare Access boundary:
|
|
||||||
- `deploy/Caddyfile.control-plane.example`
|
|
||||||
|
|
||||||
## Reliability and Diagnostics Improvements
|
|
||||||
|
|
||||||
- Agent websocket connect diagnostics now include:
|
|
||||||
- DNS resolution timing (`dns_resolve_ms`)
|
|
||||||
- websocket connect timing (`ws_connect_ms`)
|
|
||||||
- Control-plane websocket lifecycle logs include:
|
|
||||||
- connect-to-hello timing
|
|
||||||
- connect-to-auth timing
|
|
||||||
- hello-to-auth timing
|
|
||||||
- Slow connect warnings are now emitted when timing thresholds are exceeded.
|
|
||||||
|
|
||||||
## Root-Cause Findings Captured
|
|
||||||
|
|
||||||
- Long agent websocket connect delays were reproduced and traced to hostname resolution path.
|
|
||||||
- Switching agent `server_url` hostname to direct IP made connect immediate.
|
|
||||||
- This confirms app-level relay logic was not the source of the startup delay.
|
|
||||||
|
|
||||||
## Verification Status
|
|
||||||
|
|
||||||
- `cargo check --workspace` passing after all changes.
|
|
||||||
- Added and passing tests include:
|
|
||||||
- audit event append/filter in state store
|
|
||||||
- alert transition open/resolve persistence
|
|
||||||
- alert evaluator rule checks (offline + timeout, auth/enroll rejection spikes)
|
|
||||||
|
|
||||||
## Current API Surface for UI Start
|
|
||||||
|
|
||||||
- Agents and command execution:
|
|
||||||
- `GET /api/v1/control/agents`
|
|
||||||
- `POST /api/v1/control/agents/{agent_id}/command`
|
|
||||||
- Audits:
|
|
||||||
- `GET /api/v1/control/audit/events`
|
|
||||||
- Alerts:
|
|
||||||
- `GET /api/v1/control/alerts`
|
|
||||||
- `GET /api/v1/control/alerts/history`
|
|
||||||
- websocket subscribe: `GET /api/v1/control/alerts/ws`
|
|
||||||
|
|
||||||
## Remaining Plan Items (Most Significant)
|
|
||||||
|
|
||||||
1. UI implementation (`/ui` app shell and pages) is still open.
|
|
||||||
2. Alert dedupe/cooldown persistence and tuning are still basic and need hardening.
|
|
||||||
3. Audit retention pruning policy and long-run storage controls are not finalized.
|
|
||||||
4. Edge auth enforcement tests and deployment rehearsals remain to be added.
|
|
||||||
5. Multi-day soak drills and failure-injection validation remain open.
|
|
||||||
|
|
||||||
## Suggested Next Actions
|
|
||||||
|
|
||||||
1. Build minimal UI shell with three views:
|
|
||||||
- agents/commands
|
|
||||||
- audit timeline
|
|
||||||
- alerts panel (active + history + websocket stream)
|
|
||||||
2. Add periodic retention task for audit and alert transition trees.
|
|
||||||
3. Add proxy-level integration tests that assert private endpoints are blocked without Access headers.
|
|
||||||
4. Run a 48-72h soak with hostname vs IP connect-path metrics collected.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Control Plane Review
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
|
|
||||||
1. High: the admin/control API is effectively unauthenticated, so anyone who can reach the server can issue enroll tokens, list/revoke tokens, inspect audit/alerts, and send live commands to agents. In [`wakey-control-plane/src/runtime/mod.rs:61`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/runtime/mod.rs#L61) through [`wakey-control-plane/src/runtime/mod.rs:109`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/runtime/mod.rs#L109), `control_api_routes()` is merged straight into the app with no auth middleware despite the comment saying these routes are “admin-only.” The UI also calls those endpoints directly with plain `fetch` and no auth material in [`ui/src/api.ts:60`](c:/Users/Admin/Documents/realshit/wakey/ui/src/api.ts#L60) through [`ui/src/api.ts:123`](c:/Users/Admin/Documents/realshit/wakey/ui/src/api.ts#L123). This is a full remote-takeover issue for the control plane, not just a missing polish item.
|
|
||||||
|
|
||||||
2. High: a second websocket connection for the same `agent_id` silently replaces the current session, but the old authenticated socket is left alive and can still submit `result`/`error` frames against pending requests. In [`wakey-control-plane/src/ws.rs:195`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/ws.rs#L195) through [`wakey-control-plane/src/ws.rs:200`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/ws.rs#L200), a successful auth simply overwrites `sessions[agent_id] = tx.clone()`. The previous socket is not closed or demoted. Later, any authenticated socket can satisfy pending requests in [`wakey-control-plane/src/ws.rs:229`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/ws.rs#L229) through [`wakey-control-plane/src/ws.rs:247`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/ws.rs#L247), while requests are correlated only by `request_id` created in [`wakey-control-plane/src/api/commands.rs:91`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/api/commands.rs#L91) through [`wakey-control-plane/src/api/commands.rs:97`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/api/commands.rs#L97). That creates a split-brain/race condition where a stale or malicious prior session for the same agent can inject or win replies.
|
|
||||||
|
|
||||||
3. Medium: configured seed enroll tokens are reinserted into the database on every startup, so “one-time” tokens become reusable after a restart if they remain in config. In [`wakey-control-plane/src/state/store.rs:52`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/state/store.rs#L52) through [`wakey-control-plane/src/state/store.rs:68`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/state/store.rs#L68), `load_or_init()` blindly seeds `daemon.enroll_tokens` into sled every time the process starts. Enrollment consumes tokens in [`wakey-control-plane/src/state/store.rs:91`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/state/store.rs#L91) through [`wakey-control-plane/src/state/store.rs:109`](c:/Users/Admin/Documents/realshit/wakey/wakey-control-plane/src/state/store.rs#L109), but that consumption is undone on the next boot if the token still exists in config. That breaks the “short-lived/one-time” assumption and makes operational mistakes much more likely.
|
|
||||||
|
|
||||||
## Residual Risks
|
|
||||||
|
|
||||||
- The control-plane/UI surface is growing quickly and currently assumes a trusted environment in multiple places. Even after adding admin auth, I would expect more authz/session-boundary issues to surface.
|
|
||||||
- The command relay path is conceptually good, but it needs stronger session ownership rules before it is trustworthy under reconnect races or duplicated agents.
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
## Plan: Audit, Alerts, UI, and Safe Edge Exposure
|
|
||||||
|
|
||||||
Recommended approach: ship in this order audit first, alerts second, UI third, then edge hardening and soak. This gives you observability truth before you build subscriptions and screens, and keeps risky control APIs private behind Cloudflare Access at Caddy.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
1. Phase A: Lock trust boundaries and endpoint classes.
|
|
||||||
2. Public Agent API stays exposed: /api/v1/agents/enroll, /api/v1/agent/ws, /healthz.
|
|
||||||
3. Private Control API stays protected: /api/v1/control/* and /ui/*.
|
|
||||||
4. Phase A: Define AuditEvent schema and retention defaults.
|
|
||||||
5. Phase B: Add audit persistence in sled and emit at key points.
|
|
||||||
6. Emit audit on token issue/list/revoke, ws auth/disconnect, command dispatch/result/timeout/error, and reload/config operations.
|
|
||||||
7. Phase B: Add audit query API with filters and pagination.
|
|
||||||
8. Phase C: Implement deterministic alert rules and evaluator loop.
|
|
||||||
9. Start with offline agent threshold, timeout-rate threshold, auth-failure spike, token misuse attempts.
|
|
||||||
10. Add dedupe and cooldown so alerts do not flap.
|
|
||||||
11. Phase C: Add alert delivery APIs.
|
|
||||||
12. Poll-first endpoint for active alerts and recent transitions, websocket stream optional after rules stabilize.
|
|
||||||
13. Phase D: Build same-domain UI app shell at /ui with origin-relative API client.
|
|
||||||
14. Phase D: Build pages: Agent Health, Command Runner, Audit Timeline, Alerts Panel.
|
|
||||||
15. Phase E: Add Caddy deployment template with Cloudflare Access policy boundaries and websocket support.
|
|
||||||
16. Phase E: Run end-to-end drills and 48-72h soak.
|
|
||||||
|
|
||||||
**Relevant files**
|
|
||||||
- [wakey-control-plane/src/runtime/mod.rs](wakey-control-plane/src/runtime/mod.rs)
|
|
||||||
- [wakey-control-plane/src/api/commands.rs](wakey-control-plane/src/api/commands.rs)
|
|
||||||
- [wakey-control-plane/src/api/control.rs](wakey-control-plane/src/api/control.rs)
|
|
||||||
- [wakey-control-plane/src/ws.rs](wakey-control-plane/src/ws.rs)
|
|
||||||
- [wakey-control-plane/src/state/store.rs](wakey-control-plane/src/state/store.rs)
|
|
||||||
- [wakey-control-plane/src/state/types.rs](wakey-control-plane/src/state/types.rs)
|
|
||||||
- [wakey-control-plane/src/config/types.rs](wakey-control-plane/src/config/types.rs)
|
|
||||||
- [wakey-control-plane/src/config/resolve.rs](wakey-control-plane/src/config/resolve.rs)
|
|
||||||
- [wakey-control-plane/src/cli.rs](wakey-control-plane/src/cli.rs)
|
|
||||||
- [README.md](README.md)
|
|
||||||
- [scripts/init/openwrt/wakey](scripts/init/openwrt/wakey)
|
|
||||||
- [.github/plan-controlPlaneAppV1.prompt.md](.github/plan-controlPlaneAppV1.prompt.md)
|
|
||||||
|
|
||||||
**Verification**
|
|
||||||
1. Unit tests for audit append/query, pagination, retention pruning.
|
|
||||||
2. Unit tests for alert rule evaluation, dedupe, cooldown.
|
|
||||||
3. Contract tests for request_id correlation across command result and timeout/error audit records.
|
|
||||||
4. Integration tests for ws auth/disconnect audit events and command timeout event emission.
|
|
||||||
5. API tests for audit and alert endpoints.
|
|
||||||
6. Edge security tests that unauthenticated /api/v1/control/* and /ui/* are denied.
|
|
||||||
7. Soak tests for reconnect churn and audit growth stability.
|
|
||||||
|
|
||||||
**Decisions captured**
|
|
||||||
- Admin auth default: Cloudflare Access only, enforced at Caddy.
|
|
||||||
- UI host: same domain path deployment.
|
|
||||||
- Shell bridge: excluded from v1 due high risk and low break-glass value during hard router failures.
|
|
||||||
- Alert delivery: poll-first in v1, websocket stream optional.
|
|
||||||
|
|
||||||
**Caddy policy shape for this plan**
|
|
||||||
1. Route /api/v1/agents/enroll and /api/v1/agent/ws to control-plane upstream without Cloudflare Access gate.
|
|
||||||
2. Route /api/v1/control/* and /ui/* only when Cloudflare Access authentication is valid.
|
|
||||||
3. Preserve websocket upgrade headers on /api/v1/agent/ws.
|
|
||||||
4. Keep control-plane process bound to private interface or localhost behind Caddy.
|
|
||||||
5. Deny direct exposure of /api/v1/control/* from origin network paths.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
## Plan: Control Plane App v1
|
|
||||||
|
|
||||||
Build an end-to-end, ops-ready v1 over 6+ weeks by reusing current wakey and wakey-core logic, keeping wakey-agent as outbound executor, and adding a dedicated control-plane server plus minimal operator UI.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
1. Phase 1, contract baseline: finalize relay contract for command, result, error, request correlation, timeout, retry, and forward compatibility behavior.
|
|
||||||
2. Phase 1, boundary lock: keep execution in wakey service functions and keep domain DTOs in wakey-core while removing legacy HTTP/static compatibility code.
|
|
||||||
3. Phase 2, server skeleton: implement enrollment endpoint, agent registry, websocket acceptor, and request correlation map. Depends on step 1 and step 2.
|
|
||||||
4. Phase 2, relay core: implement command submission to connected agents, request_id correlation, timeout paths, and structured relay errors. Depends on step 3.
|
|
||||||
5. Phase 2, persistence and identity: durable agent records, enroll token lifecycle, and safe credential metadata. Parallel with step 4 after schema is stable.
|
|
||||||
6. Phase 3, operator surface: add API for agent inventory, health, command execution, and recent outcomes; add minimal UI for core operations. Depends on step 4 and step 5.
|
|
||||||
7. Phase 3, ops hardening: metrics, logs, audits, heartbeat liveness checks, and alert thresholds. Parallel with step 6.
|
|
||||||
8. Phase 4, deployment pipeline: add server build and deploy artifacts, environment templates, and rollback workflow. Depends on step 6 and step 7.
|
|
||||||
9. Phase 4, validation and soak: run enrollment-to-command end-to-end tests and disconnect/failure drills with multi-day soak. Depends on step 8.
|
|
||||||
|
|
||||||
**Relevant files to reuse**
|
|
||||||
- [wakey-agent/src/protocol.rs](wakey-agent/src/protocol.rs)
|
|
||||||
- [wakey-agent/src/session.rs](wakey-agent/src/session.rs)
|
|
||||||
- [wakey-agent/src/dispatch.rs](wakey-agent/src/dispatch.rs)
|
|
||||||
- [src/service/mod.rs](src/service/mod.rs)
|
|
||||||
- [wakey-core/src/model](wakey-core/src/model)
|
|
||||||
- [scripts/package_rootfs.ps1](scripts/package_rootfs.ps1)
|
|
||||||
- [.gitea/workflows/release.yml](.gitea/workflows/release.yml)
|
|
||||||
|
|
||||||
**Verification**
|
|
||||||
1. Contract tests for command and result serialization, request_id stability, and unknown frame tolerance.
|
|
||||||
2. Relay integration tests for enrollment, websocket auth flow, correlation, and timeout handling.
|
|
||||||
3. Security tests for token lifecycle and invalid credential rejection.
|
|
||||||
4. API tests for registry and command execution behavior.
|
|
||||||
5. Observability tests for heartbeat, reconnect counters, latency, and failure alerts.
|
|
||||||
6. Deployment tests for build, release, rollback rehearsal, and staging smoke checks.
|
|
||||||
7. On-device tests for enroll, procd lifecycle, reconnect, and remote command round trips.
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
## Plan: wakey UI v1 (Device-First)
|
|
||||||
|
|
||||||
The Operator UI is not the end goal by itself.
|
|
||||||
The goal is to make wakey excellent at its original purpose: quickly finding devices and waking them reliably.
|
|
||||||
|
|
||||||
Agent, audit, and token features remain important, but they should support the device workflow rather than dominate the navigation and development effort.
|
|
||||||
|
|
||||||
## Product North Star
|
|
||||||
|
|
||||||
An operator should be able to do this in under 10 seconds:
|
|
||||||
1. Open the UI.
|
|
||||||
2. Search for a device by name, IP, or MAC.
|
|
||||||
3. See whether it looks online/reachable.
|
|
||||||
4. Trigger wake.
|
|
||||||
5. See immediate command result and short follow-up status.
|
|
||||||
|
|
||||||
## Scope Priorities
|
|
||||||
|
|
||||||
1. P0: Device discovery and wake UX.
|
|
||||||
2. P1: Fast troubleshooting context around wake results.
|
|
||||||
3. P2: Fleet/agent/admin operations.
|
|
||||||
|
|
||||||
This explicitly means pages and components for Agent, Audit, Alerts, and Tokens should be present but secondary in visual hierarchy and effort until P0 is complete.
|
|
||||||
|
|
||||||
## IA Direction (v1)
|
|
||||||
|
|
||||||
Primary top-level focus:
|
|
||||||
1. Devices
|
|
||||||
2. Wake Queue (or Recent Actions)
|
|
||||||
|
|
||||||
Secondary top-level focus:
|
|
||||||
1. Fleet Health
|
|
||||||
2. Audit
|
|
||||||
3. Alerts
|
|
||||||
4. Access/Tokens
|
|
||||||
|
|
||||||
If needed, keep current routes during transition, but adjust default landing and navigation emphasis so Devices is the home workflow.
|
|
||||||
|
|
||||||
## Phase Plan
|
|
||||||
|
|
||||||
### Phase 1: Device-Centric Foundation
|
|
||||||
|
|
||||||
1. Add a dedicated Devices page that merges existing status/leases/inventory signal into one operator list.
|
|
||||||
2. Include searchable columns for name, IP, MAC, interface/dev, and recency indicators.
|
|
||||||
3. Provide row-level wake action and bulk-safe interaction model (single-click row action, confirm for bulk).
|
|
||||||
4. Define a compact "device confidence" heuristic from available data (for example: recent lease + reachable neighbor).
|
|
||||||
5. Set the default route to Devices, with prominent search and wake controls above the fold.
|
|
||||||
|
|
||||||
Acceptance criteria:
|
|
||||||
1. Search by hostname, IP, and MAC all work from one input.
|
|
||||||
2. Wake action reachable in one click from list row.
|
|
||||||
3. Response feedback shown immediately with clear success/error text.
|
|
||||||
|
|
||||||
### Phase 2: Wake Execution UX
|
|
||||||
|
|
||||||
1. Build a focused wake panel with explicit target preview before send.
|
|
||||||
2. Provide quick presets: "wake by selected device", "wake by MAC", "wake by query".
|
|
||||||
3. Persist recent wake targets locally for operator speed.
|
|
||||||
4. Add post-wake verification loop (short timed refresh of status indicators).
|
|
||||||
5. Surface request correlation id and a copy action for incident sharing.
|
|
||||||
|
|
||||||
Acceptance criteria:
|
|
||||||
1. Operator can retry wake with one click.
|
|
||||||
2. Operator can see last 20 wake attempts with outcome and timestamp.
|
|
||||||
3. Error states differentiate validation, timeout, and execution failure.
|
|
||||||
|
|
||||||
### Phase 3: Context Without Workflow Drift
|
|
||||||
|
|
||||||
1. Keep Alerts and Audit accessible from device rows and wake outcomes.
|
|
||||||
2. Add contextual deep links: device -> related alerts, device -> recent audit events.
|
|
||||||
3. Improve filtering for alerts/audit with saved local filter presets.
|
|
||||||
4. Add gentle live updates, keeping websocket optional with polling fallback.
|
|
||||||
|
|
||||||
Acceptance criteria:
|
|
||||||
1. From any failed wake, operator can jump to relevant audit entries in one step.
|
|
||||||
2. Alerts page can be filtered by kind/severity/agent and linked back to impacted devices.
|
|
||||||
|
|
||||||
### Phase 4: Fleet/Admin Hardening
|
|
||||||
|
|
||||||
1. Keep Agents page for connectivity and control routing visibility.
|
|
||||||
2. Keep Tokens page for enrollment lifecycle operations.
|
|
||||||
3. Keep Dashboard but reframe metrics around "device availability" and "wake success" first.
|
|
||||||
4. Add audit-friendly confirmation flows for destructive actions.
|
|
||||||
|
|
||||||
Acceptance criteria:
|
|
||||||
1. Admin flows do not block or slow P0 device workflows.
|
|
||||||
2. All admin actions produce clear audit-visible outcomes.
|
|
||||||
|
|
||||||
### Phase 5: Validation and Production Readiness
|
|
||||||
|
|
||||||
1. Add smoke tests for P0 flow: find device -> wake -> observe result.
|
|
||||||
2. Add contract checks for command payload/response shapes used by device and wake screens.
|
|
||||||
3. Add scenario drills (offline agent, delayed command, websocket drop, expired token).
|
|
||||||
4. Verify edge policy still protects control routes while preserving required public endpoints.
|
|
||||||
|
|
||||||
Acceptance criteria:
|
|
||||||
1. P0 flow remains usable during partial degradation.
|
|
||||||
2. Build/typecheck/test gates stay green in CI.
|
|
||||||
|
|
||||||
## Concrete UI Backlog (Ordered)
|
|
||||||
|
|
||||||
1. Create DevicesPage with unified searchable table.
|
|
||||||
2. Wire wake action directly from device row.
|
|
||||||
3. Add Recent Wake Actions panel with outcomes.
|
|
||||||
4. Add post-wake short verification refresh.
|
|
||||||
5. Rework Dashboard cards to device-first metrics.
|
|
||||||
6. Add deep links from wake result to audit and alerts context.
|
|
||||||
7. Add empty/loading/error skeleton patterns tuned for device list scale.
|
|
||||||
|
|
||||||
## Metrics of Success
|
|
||||||
|
|
||||||
1. Time-to-wake median under 10 seconds for known target.
|
|
||||||
2. Wake success rate visible per time window.
|
|
||||||
3. Fewer operator clicks for common tasks (device lookup + wake).
|
|
||||||
4. Reduced navigation to agent-centric pages for everyday usage.
|
|
||||||
|
|
||||||
## Design and Interaction Principles
|
|
||||||
|
|
||||||
1. Device-first information density over generic admin dashboards.
|
|
||||||
2. Search and action bar always visible on desktop and mobile.
|
|
||||||
3. Action feedback must be immediate and explicit.
|
|
||||||
4. Keep advanced infrastructure details available but visually de-emphasized.
|
|
||||||
|
|
||||||
## Architecture Decisions
|
|
||||||
|
|
||||||
1. Keep same-domain /ui serving and origin-relative API requests.
|
|
||||||
2. Keep route and API contracts stable while iterating UX aggressively.
|
|
||||||
3. Treat websocket as progressive enhancement; polling fallback required.
|
|
||||||
4. Preserve Cloudflare Access boundary for all control endpoints.
|
|
||||||
|
|
||||||
## Relevant Files
|
|
||||||
|
|
||||||
1. [ui/src/App.tsx](ui/src/App.tsx)
|
|
||||||
2. [ui/src/pages/CommandsPage.tsx](ui/src/pages/CommandsPage.tsx)
|
|
||||||
3. [ui/src/pages/DashboardPage.tsx](ui/src/pages/DashboardPage.tsx)
|
|
||||||
4. [ui/src/api.ts](ui/src/api.ts)
|
|
||||||
5. [wakey-control-plane/src/api/commands.rs](wakey-control-plane/src/api/commands.rs)
|
|
||||||
6. [wakey-control-plane/src/api/audit.rs](wakey-control-plane/src/api/audit.rs)
|
|
||||||
7. [wakey-control-plane/src/api/alerts.rs](wakey-control-plane/src/api/alerts.rs)
|
|
||||||
8. [wakey-control-plane/src/api/control.rs](wakey-control-plane/src/api/control.rs)
|
|
||||||
9. [wakey-control-plane/src/runtime/mod.rs](wakey-control-plane/src/runtime/mod.rs)
|
|
||||||
10. [README.md](README.md)
|
|
||||||
|
|
||||||
## Immediate Next Build Slice
|
|
||||||
|
|
||||||
1. Implement DevicesPage and make it default route.
|
|
||||||
2. Add row-level wake action with in-place result feedback.
|
|
||||||
3. Add Recent Wake Actions section backed by local state first, then audit correlation.
|
|
||||||
4. Rebalance navigation labels/order to make device workflows primary.
|
|
||||||
@@ -7,10 +7,6 @@ on:
|
|||||||
- "v*"
|
- "v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
|
||||||
# When using cargo-zigbuild for *-gnu targets, link against this glibc baseline (wider VPS compatibility).
|
|
||||||
WAKEY_CC_GLIBC_VERSION: "2.28"
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
@@ -23,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: armv7-unknown-linux-musleabihf,x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu
|
targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
@@ -35,14 +31,8 @@ jobs:
|
|||||||
cache: pnpm
|
cache: pnpm
|
||||||
cache-dependency-path: ui/pnpm-lock.yaml
|
cache-dependency-path: ui/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Ensure toolchain and targets
|
- name: Install cross
|
||||||
shell: pwsh
|
run: cargo install cross --locked
|
||||||
run: |
|
|
||||||
rustc -V
|
|
||||||
cargo -V
|
|
||||||
rustup target add armv7-unknown-linux-musleabihf
|
|
||||||
rustup target add x86_64-unknown-linux-gnu
|
|
||||||
rustup target add aarch64-unknown-linux-gnu
|
|
||||||
|
|
||||||
- name: Cache cargo
|
- name: Cache cargo
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -53,10 +43,9 @@ jobs:
|
|||||||
target
|
target
|
||||||
key: linux-cargo-${{ hashFiles('**/Cargo.lock') }}
|
key: linux-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Build (armv7-musl)
|
- name: Build (armv7-musl with cross)
|
||||||
shell: pwsh
|
|
||||||
run: |
|
run: |
|
||||||
cargo build --release --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent
|
cross build --release --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent
|
||||||
|
|
||||||
- name: Build UI dist
|
- name: Build UI dist
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -66,29 +55,17 @@ jobs:
|
|||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
- name: Build control-plane (linux-gnu; zig + older glibc when available)
|
- name: Build control-plane (linux-gnu)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
GLIBC_VER="${WAKEY_CC_GLIBC_VERSION:-2.28}"
|
|
||||||
GNU_TARGETS=(x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu)
|
|
||||||
|
|
||||||
use_zig=false
|
# x86_64 builds natively; aarch64 needs cross
|
||||||
if command -v zig >/dev/null 2>&1 && cargo zigbuild -h >/dev/null 2>&1; then
|
echo "Building control-plane x86_64-unknown-linux-gnu (native)"
|
||||||
use_zig=true
|
cargo build --release --target x86_64-unknown-linux-gnu -p wakey-control-plane
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$use_zig" = true ]; then
|
echo "Building control-plane aarch64-unknown-linux-gnu (cross)"
|
||||||
echo "cargo-zigbuild + zig found: building control-plane with glibc ${GLIBC_VER}"
|
cross build --release --target aarch64-unknown-linux-gnu -p wakey-control-plane
|
||||||
for base in "${GNU_TARGETS[@]}"; do
|
|
||||||
cargo zigbuild --release --target "${base}.${GLIBC_VER}" -p wakey-control-plane
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "zig or cargo zigbuild not available: using plain cargo build (binary needs glibc >= host)"
|
|
||||||
for base in "${GNU_TARGETS[@]}"; do
|
|
||||||
cargo build --release --target "$base" -p wakey-control-plane
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Package rootfs tarball
|
- name: Package rootfs tarball
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|||||||
@@ -18,3 +18,5 @@ target/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
.codex
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/cache
|
||||||
|
/project.local.yml
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Completion Checklist
|
||||||
|
|
||||||
|
Before handing back code changes, run the smallest checks covering the changed area:
|
||||||
|
- Rust formatting: `cargo fmt`.
|
||||||
|
- For control-plane SQLx/state/API work: `cargo test -p wakey-control-plane` and `SQLX_OFFLINE=true cargo test -p wakey-control-plane`.
|
||||||
|
- For SQLx query macro changes: run `scripts/prepare_sqlx_sqlite.sh` and include the `.sqlx` metadata changes.
|
||||||
|
- For wakey-linux/core inventory/observation work: `cargo test -p wakey-linux -p wakey-core`.
|
||||||
|
- For agent session/config/protocol work: `cargo test -p wakey-agent`.
|
||||||
|
- For UI work: `pnpm --dir ui typecheck` and `pnpm --dir ui build`; use `pnpm --dir ui format` when touching TS/TSX/CSS formatting.
|
||||||
|
- Run relevant `cargo clippy -p <crate> --all-targets --all-features` when behavior or API shape changes.
|
||||||
|
|
||||||
|
Always check `git status --short` and mention unrelated dirty files or generated metadata in the final summary.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Wakey Project Overview
|
||||||
|
|
||||||
|
Wakey is a Wake-on-LAN and LAN observability workspace for Linux/OpenWrt routers. It includes:
|
||||||
|
- `wakey-core`: shared model and parsing types for devices, neighbors, DHCP, interfaces, wake results.
|
||||||
|
- `wakey-linux`: Linux/OpenWrt adapter layer for DHCP lease files, neighbor lookup, interface data, WoL sending, and local observation storage.
|
||||||
|
- root `wakey` crate: service layer and operator CLI for inventory, leases, devs, wake, and observe.
|
||||||
|
- `wakey-agent`: outbound router daemon. Enrolls with control plane, maintains authenticated websocket, executes relayed commands, syncs observations.
|
||||||
|
- `wakey-control-plane`: Axum control plane, SQLx SQLite state, agent registry, command relay, known devices, observations, audit/alerts, fleet APIs, static UI hosting.
|
||||||
|
- `ipjs`: typed wrappers around Linux `ip -j` data.
|
||||||
|
- `ui`: React/Vite operator UI using shadcn/base-ui style components.
|
||||||
|
|
||||||
|
Current persistence direction: control plane uses SQLx SQLite with migrations in `wakey-control-plane/migrations` and offline query metadata in `.sqlx` plus `wakey-control-plane/.sqlx`. Legacy sled import exists as explicit command.
|
||||||
|
|
||||||
|
Current product direction: fleet-first device UI and backend. Known device identity is manual. Observations/inventory provide suggestions/current state but should not automatically promote or delete durable identifiers.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Style And Conventions
|
||||||
|
|
||||||
|
Rust:
|
||||||
|
- Edition 2024.
|
||||||
|
- Prefer explicit, small service/API functions over large abstractions.
|
||||||
|
- Use `anyhow::Context` for fallible operations in application layers.
|
||||||
|
- Use `tracing` for logs.
|
||||||
|
- Shared domain types live in `wakey-core`; Linux/OpenWrt I/O in `wakey-linux`; root crate owns service/CLI behavior; agent/control-plane should reuse service/domain types where possible.
|
||||||
|
- SQLx query macros are used in control-plane state. Regenerate `.sqlx` metadata after query string changes.
|
||||||
|
- Device identity is manual: observations and inventory may suggest, but should not silently pin, merge, promote, or delete known identifiers.
|
||||||
|
- Hotplug observation actions should stay event-shaped (`add`, `update`, `remove`). Do not store NUD state as the action. NUD belongs to live neighbor/inventory data.
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
- React + Vite + TypeScript.
|
||||||
|
- Use existing component/style conventions; shadcn/base-ui/lucide are available.
|
||||||
|
- Main operator UI is fleet-first: common path is find device, inspect current IP/MAC/agent, wake, copy, remember/merge.
|
||||||
|
- Admin/debug pages stay available but should not dominate the primary nav.
|
||||||
|
|
||||||
|
Editing:
|
||||||
|
- Keep changes scoped.
|
||||||
|
- Preserve user edits in dirty worktrees.
|
||||||
|
- Use `apply_patch` for manual file edits.
|
||||||
|
- Default to ASCII unless files already use non-ASCII for a reason.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Suggested Commands
|
||||||
|
|
||||||
|
Rust formatting and checks:
|
||||||
|
- `cargo fmt`
|
||||||
|
- `cargo test -p wakey-control-plane`
|
||||||
|
- `SQLX_OFFLINE=true cargo test -p wakey-control-plane`
|
||||||
|
- `cargo test -p wakey-linux -p wakey-core`
|
||||||
|
- `cargo test -p wakey-agent` (may need elevated permissions if tests bind local sockets)
|
||||||
|
- `cargo clippy -p wakey-control-plane --all-targets --all-features`
|
||||||
|
- `cargo clippy -p wakey-linux -p wakey-core --all-targets --all-features`
|
||||||
|
|
||||||
|
SQLx metadata after query macro changes:
|
||||||
|
- `scripts/prepare_sqlx_sqlite.sh`
|
||||||
|
|
||||||
|
UI checks:
|
||||||
|
- `pnpm --dir ui typecheck`
|
||||||
|
- `pnpm --dir ui build`
|
||||||
|
- `pnpm --dir ui format`
|
||||||
|
|
||||||
|
Local run examples:
|
||||||
|
- `cargo run -- inventory <query>`
|
||||||
|
- `cargo run -- leases`
|
||||||
|
- `cargo run -- devs`
|
||||||
|
- `cargo run -- wake --mac aa:bb:cc:dd:ee:ff`
|
||||||
|
- `cargo run -p wakey-agent -- serve --config /etc/wakey-agent/config.toml`
|
||||||
|
- `cargo run -p wakey-control-plane -- serve --config-file /etc/wakey-control-plane/config.toml`
|
||||||
|
|
||||||
|
Useful shell tools:
|
||||||
|
- Prefer `rg` and `rg --files` for search.
|
||||||
|
- Use `git status --short`, `git diff --stat`, and focused `git diff -- <path>` before edits.
|
||||||
|
- Avoid destructive git commands unless explicitly requested.
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# the name by which the project can be referenced within Serena
|
||||||
|
project_name: "wakey"
|
||||||
|
|
||||||
|
|
||||||
|
# list of languages for which language servers are started; choose from:
|
||||||
|
# al bash clojure cpp csharp
|
||||||
|
# csharp_omnisharp dart elixir elm erlang
|
||||||
|
# fortran fsharp go groovy haskell
|
||||||
|
# haxe java julia kotlin lua
|
||||||
|
# markdown
|
||||||
|
# matlab nix pascal perl php
|
||||||
|
# php_phpactor powershell python python_jedi r
|
||||||
|
# rego ruby ruby_solargraph rust scala
|
||||||
|
# swift terraform toml typescript typescript_vts
|
||||||
|
# vue yaml zig
|
||||||
|
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||||
|
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||||
|
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||||
|
# Note:
|
||||||
|
# - For C, use cpp
|
||||||
|
# - For JavaScript, use typescript
|
||||||
|
# - For Free Pascal/Lazarus, use pascal
|
||||||
|
# Special requirements:
|
||||||
|
# Some languages require additional setup/installations.
|
||||||
|
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||||
|
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||||
|
# The first language is the default language and the respective language server will be used as a fallback.
|
||||||
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
|
languages:
|
||||||
|
- rust
|
||||||
|
- typescript
|
||||||
|
|
||||||
|
# the encoding used by text files in the project
|
||||||
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
# line ending convention to use when writing source files.
|
||||||
|
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||||
|
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||||
|
line_ending:
|
||||||
|
|
||||||
|
# The language backend to use for this project.
|
||||||
|
# If not set, the global setting from serena_config.yml is used.
|
||||||
|
# Valid values: LSP, JetBrains
|
||||||
|
# Note: the backend is fixed at startup. If a project with a different backend
|
||||||
|
# is activated post-init, an error will be returned.
|
||||||
|
language_backend:
|
||||||
|
|
||||||
|
# whether to use project's .gitignore files to ignore files
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
|
||||||
|
# advanced configuration option allowing to configure language server-specific options.
|
||||||
|
# Maps the language key to the options.
|
||||||
|
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||||
|
# No documentation on options means no options are available.
|
||||||
|
ls_specific_settings: {}
|
||||||
|
|
||||||
|
# list of additional paths to ignore in this project.
|
||||||
|
# Same syntax as gitignore, so you can use * and **.
|
||||||
|
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude.
|
||||||
|
# This extends the existing exclusions (e.g. from the global configuration)
|
||||||
|
#
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project based on the project name or path.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
|
||||||
|
# for example by saying that the information retrieved from a memory file is no longer correct
|
||||||
|
# or no longer relevant for the project.
|
||||||
|
# * `edit_memory`: Replaces content matching a regular expression in a memory.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_file`: Finds files in the given relative paths
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
|
||||||
|
# * `find_symbol`: Performs a global (or local) search using the language server backend.
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
|
||||||
|
# for clients that do not read the initial instructions when the MCP server is connected.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
|
||||||
|
# is relevant to the current task. You can infer whether the information
|
||||||
|
# is relevant from the memory file name.
|
||||||
|
# You should not read the same memory file multiple times in the same conversation.
|
||||||
|
# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
|
||||||
|
# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
|
||||||
|
# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
|
||||||
|
# For JB, we use a separate tool.
|
||||||
|
# * `replace_content`: Replaces content in a file (optionally using regular expressions).
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
|
||||||
|
# * `safe_delete_symbol`:
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
|
||||||
|
# The memory name should be meaningful.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||||
|
# This extends the existing inclusions (e.g. from the global configuration).
|
||||||
|
included_optional_tools: []
|
||||||
|
|
||||||
|
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||||
|
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||||
|
fixed_tools: []
|
||||||
|
|
||||||
|
# list of mode names to that are always to be included in the set of active modes
|
||||||
|
# The full set of modes to be activated is base_modes + default_modes.
|
||||||
|
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||||
|
# Otherwise, this setting overrides the global configuration.
|
||||||
|
# Set this to [] to disable base modes for this project.
|
||||||
|
# Set this to a list of mode names to always include the respective modes for this project.
|
||||||
|
base_modes:
|
||||||
|
|
||||||
|
# list of mode names that are to be activated by default.
|
||||||
|
# The full set of modes to be activated is base_modes + default_modes.
|
||||||
|
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||||
|
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||||
|
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||||
|
default_modes:
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||||
|
# such as docstrings or parameter information.
|
||||||
|
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||||
|
# If null or missing, use the setting from the global configuration.
|
||||||
|
symbol_info_budget:
|
||||||
|
|
||||||
|
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||||
|
# Extends the list from the global configuration, merging the two lists.
|
||||||
|
read_only_memory_patterns: []
|
||||||
|
|
||||||
|
# list of regex patterns for memories to completely ignore.
|
||||||
|
# Matching memories will not appear in list_memories or activate_project output
|
||||||
|
# and cannot be accessed via read_memory or write_memory.
|
||||||
|
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||||
|
# Extends the list from the global configuration, merging the two lists.
|
||||||
|
# Example: ["_archive/.*", "_episodes/.*"]
|
||||||
|
ignored_memory_patterns: []
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO agents (agent_id, agent_token) VALUES (?1, ?2)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "020b33576bfc2d171a051f903d40ccc78c972fa9d5d983a5678f03abbc8b8488"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT OR REPLACE INTO enroll_tokens (token, expires_at_unix) VALUES (?1, ?2)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "07d7f283daba2742e5522abf99074171ed484c03f70138be611e0c4b17e311b8"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM agent_devices WHERE agent_id = ?1 AND device_key = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "09db61a070a351a6255e21d5949285ccb19fca7f8f29a6c18dddac5495257a45"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO audit_events\n (event_key, event_id, ts_unix, actor_type, actor_id, agent_id, request_id,\n event_type, outcome, latency_ms, message, metadata_json)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 12
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "0c47aade9d5c71d6c7e2023c6ff66c6ab4b3a0dd2b78af3a600dedc89da508f2"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) as \"count!: i64\" FROM enroll_tokens",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "count!: i64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "0ca58d0cbc25c59d2c956222d41c02eb0c76a135f29a56a7570b750f7378738d"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", fact_json as \"fact_json!\"\n FROM agent_device_facts",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fact_json!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "12e443ec0991c042cf527498b567bbbb6d02885f72eb3fa426c9493368f8a9e7"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO agent_meta (agent_id, nickname) VALUES (?1, ?2)\n ON CONFLICT(agent_id) DO UPDATE SET nickname = excluded.nickname",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "181992e554afdd8f21bd5df71bfb7880a5a9de28af8cd0c766d8d4aa649b3c90"
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT device_id as \"device_id!\", display_name as \"display_name!\",\n pinned, created_at_unix, updated_at_unix, notes\n FROM known_devices\n ORDER BY pinned DESC, display_name, device_id",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "device_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display_name!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pinned",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at_unix",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at_unix",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "notes",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "19b5fc20ffc99589bbaf11b94661f7c0dfcf80748c60f8f3289bb3296114635a"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) as \"count!: i64\" FROM agents",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "count!: i64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "21e5986ad3e8efd3393f2bc4929ee79e664cbc2b55948c276d52ca2a6fdc6a48"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO known_devices\n (device_id, display_name, pinned, created_at_unix, updated_at_unix, notes)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 6
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "2bff8b8b070f5b5dd946e754c5e15ea405f6c29baa4c2c31347f455227437ae2"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT 1 as \"ok!\"",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "ok!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "2c0dd5d9be90cc716da219c75c2d3b75dae8c0902c64ef5baa96c902f23abc5d"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM agent_device_hostnames WHERE agent_id = ?1 AND device_key = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "2cd47bbce9b9ae2ebf39f31b12722bb8d19ef6d90ac634731932a4cefbde03b7"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT device_key FROM agent_devices WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "device_key",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "3f24dfb51be2c3051dc8a8f732bd3e3fe9ca6b2649e1202d3725693b8011f269"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", hostname as \"hostname!\"\n FROM agent_device_hostnames\n WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hostname!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "4188fb9a0dac2e19fb1f83e023eb1e8d93f6ca896cd78a53668080639fc048d9"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", ip as \"ip!\"\n FROM agent_device_ips",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ip!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "4dd5e580826b0f6980bb8c2adecdc81ff95992ec337f93263463c5a4e357dc72"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM agent_device_macs WHERE agent_id = ?1 AND device_key = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "4f4329b0a47a00a3b881a1bc5cf8bb9855351714ff827ccff917b3e623191504"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) as \"count!: i64\" FROM enroll_tokens WHERE expires_at_unix <= ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "count!: i64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "5083fd5878c57f4d54e8b24ad0cef2771f7cac36afb7c370c65137fc0784f093"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM agents WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "51cb6ec47368803af60114a39dd9e70ba9d31946bc39980c9e85dbc4e842a2c8"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT expires_at_unix FROM enroll_tokens WHERE token = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "expires_at_unix",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "540f57604aa987b97930bb0bcfe440094d97b769fd758fd5cffc32f6d5716bb4"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM agent_device_facts WHERE agent_id = ?1 AND device_key = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "55340d50a29e0a2d324ed946b6fa6503fbd4ea0674bb7ec24e3426c5b0e5832f"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", mac as \"mac!\"\n FROM agent_device_macs\n WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mac!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "580f6bc100d373137db6f853b8babd7633c700dbfaf17ded507b704fd98e46a5"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", mac as \"mac!\"\n FROM agent_device_macs",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mac!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "5824c6cb841f7aaeb2a039f846effe17ba8038e35998b674136a19a4d42d8b88"
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id, device_key, presence, display_name,\n first_seen_unix, last_seen_unix\n FROM agent_devices\n WHERE agent_id = ?1\n ORDER BY device_key",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "presence",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display_name",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "first_seen_unix",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_seen_unix",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "5f7e043ca389f1431c7b3478ea506b0c9bb706d32c06871d1267068dbe9cb746"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) as \"count!: i64\" FROM agents WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "count!: i64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "60c6631ace995d7ca109d1bee1d878c823edd40f15f8571b42e00680a120ad74"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO alert_transitions\n (transition_key, transition_id, ts_unix, alert_id, kind, agent_id,\n from_status, to_status, message, metadata_json)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 10
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "69601f4f2268a759f03a2d533a0a30dec5cf05797817fd8d2426714cd84d7ada"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE device_identifiers SET device_id = ?1 WHERE device_id = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "7247cc488921de2313a602697590502771a231bde7d03cd9ccac55a70971da45"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM device_identifiers WHERE device_id = ?1 AND identifier_key = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "72aadefca9851a8becdd2dcce54b646aff095b64cef9837f5e8e4ea8dde71d02"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM active_alerts",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "72afd48584f822116318c623f7dd42ddf4c82b337d8a3d52ed5973d52e08c163"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO device_identifiers\n (identifier_key, device_id, kind, value, created_at_unix)\n VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 5
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "7dc7edbbe5539f89783cbccb0475e676cb6d369b676b0c5aaedf99854ffbd760"
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT token as \"token!\", expires_at_unix FROM enroll_tokens ORDER BY expires_at_unix, token",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "token!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expires_at_unix",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "7f6c5c108953cd4eda8f4e1d4f4db6b3b88270d3d7087b2948362d0ea94ad246"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM agent_meta WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "8bf8470846bd228d2ea9b243f70f946f002915b9b1983bce818abc0ae3dd97f0"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM enroll_tokens WHERE token = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "8f3499a2f461f738711b9942a9c9d6254c1f7ed0db54db8b3d65e10fa8dadfc9"
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agents.agent_id as \"agent_id!\", agent_meta.nickname\n FROM agents\n LEFT JOIN agent_meta ON agent_meta.agent_id = agents.agent_id\n ORDER BY agents.agent_id",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nickname",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "977adb8bdfb9e031a738055fb81c02c6180ca9c37c90e39b3e4efb9168bd1944"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) as \"count!: i64\" FROM meta WHERE key = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "count!: i64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9f5d340530083d040794bed2a45b01522f57dc4132184dab0eb32d1b6811992d"
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT device_id as \"device_id!\", display_name as \"display_name!\",\n pinned, created_at_unix, updated_at_unix, notes\n FROM known_devices\n WHERE device_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "device_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display_name!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pinned",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at_unix",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at_unix",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "notes",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9fe9f41d6716305ffebc764a72a3aea11e4ba348fae664aa6ba4bffbb3a2bf34"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT device_id FROM device_identifiers WHERE identifier_key = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "device_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "a1bca2de4e1328fa066d5a8703ef5b8bda560b234b9f1e75093827efeac10eec"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO agent_devices (agent_id, device_key, presence, display_name, first_seen_unix, last_seen_unix)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ON CONFLICT (agent_id, device_key) DO UPDATE SET\n presence = excluded.presence,\n display_name = excluded.display_name,\n last_seen_unix = excluded.last_seen_unix",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 6
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "a29f58cb7dd1b1bc510a0d04ecd1dbc7250824ba089dbc36ed9ac2e6dc920fd7"
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT transition_id as \"transition_id!\", ts_unix, alert_id as \"alert_id!\",\n kind as \"kind!\", agent_id, from_status, to_status as \"to_status!\",\n message as \"message!\", metadata_json as \"metadata_json!\"\n FROM alert_transitions\n ORDER BY transition_key DESC\n LIMIT ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "transition_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ts_unix",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "alert_id!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kind!",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "agent_id",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_status",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to_status!",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "message!",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_json!",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "a34accb5d8ef1fc90e1029b808956c85fa7cb00c0e879f27bbb99b5a76b5e151"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\" FROM agents ORDER BY agent_id",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "a5baac2d2f6dc38946ff15f6cba46ac085780e6521ba23a605a78b4b92a62073"
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT alert_id as \"alert_id!\", kind as \"kind!\", severity as \"severity!\",\n status as \"status!\", agent_id, message as \"message!\",\n value, threshold, last_seen_unix, metadata_json as \"metadata_json!\"\n FROM active_alerts",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "alert_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kind!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "severity!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status!",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "agent_id",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "message!",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "threshold",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_seen_unix",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_json!",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "b031d0fa0f9d0640555fc78b400b7bb951a3f013715fb5be51030c2be63e864d"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM known_devices WHERE device_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "b1c6150ef7bd67ce78f8934fc5d451b36f5bed834d39bdde01356e331c5e5ddb"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT value FROM meta WHERE key = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Blob"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "b805f8dada38af1c79a7b3bbc063fd949bf81757705ace7173658de9f185868b"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO enroll_tokens (token, expires_at_unix) VALUES (?1, ?2)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "bee62978a2ce59095f9bc679258b5747e074a55d52c43b982ea019c79aff1479"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", ip as \"ip!\"\n FROM agent_device_ips\n WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ip!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c423e005d85659547302f506380ad217cc31329262a1c0254ef532653b3a37bb"
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT transition_id as \"transition_id!\", ts_unix, alert_id as \"alert_id!\",\n kind as \"kind!\", agent_id, from_status, to_status as \"to_status!\",\n message as \"message!\", metadata_json as \"metadata_json!\"\n FROM alert_transitions\n WHERE ts_unix >= ?1\n ORDER BY transition_key DESC\n LIMIT ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "transition_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ts_unix",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "alert_id!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kind!",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "agent_id",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_status",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to_status!",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "message!",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_json!",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "ca117e134d45464a8930d0483ff8670d2798f38718c67f49adce06f74656bf2c"
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id, device_key, presence, display_name,\n first_seen_unix, last_seen_unix\n FROM agent_devices\n ORDER BY agent_id, device_key",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "presence",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "display_name",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "first_seen_unix",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_seen_unix",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "cd8585b9431d223962c376a2d463f0ca8128e7759c8694631f41db0cbd8eb221"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE known_devices SET updated_at_unix = ?1 WHERE device_id = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d2c2ce1761d22477164b3e5cfde9718877b3b472886ae1dc7e64dc36ad75b2ea"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM agent_device_ips WHERE agent_id = ?1 AND device_key = ?2",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d442949dce7a4b9a8af97dc34fc34608029564740ad327c525c7f1466bc18a11"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", hostname as \"hostname!\"\n FROM agent_device_hostnames",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hostname!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "dba637bf03d53bf3394ead0f01466f9a72fafc54f9ce239d2e5ac7f76b63b168"
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT identifier_key as \"identifier_key!\", device_id as \"device_id!\",\n kind as \"kind!\", value as \"value!\", created_at_unix\n FROM device_identifiers\n WHERE device_id = ?1\n ORDER BY kind, value",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "identifier_key!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_id!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kind!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value!",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at_unix",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "dd1d6ba80d6380cb07227eac0b90951c93f63dc4c89349cf1920405943e2093a"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_token FROM agents WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_token",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "e0def02317412d21994af8d7770a9e717f4be97e022c0f2022983d59391c775b"
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT agent_id as \"agent_id!\", device_key as \"device_key!\", fact_json as \"fact_json!\"\n FROM agent_device_facts\n WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "agent_id!",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "device_key!",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fact_json!",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "e6be4d9782f66f98ebe4d4b6ec6e93ea9efe8e4c7f1147e1ff68d41a311d57f9"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM enroll_tokens WHERE expires_at_unix <= ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ee6d28dc501f6bb8cf1249ad03b82e3554f1167d5056f02a0cc823601f480e53"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) as \"count!: i64\" FROM known_devices WHERE device_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "count!: i64",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "f18ab386ab1961e938ae7fec3e752bdd958118aa4ca81cab3421ac5827b04029"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) FROM enroll_tokens WHERE token = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "COUNT(*)",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "f5a815d4d6e3d088aba3cd01607764d89afaae6108aad0d17e93b1bfc9f7e772"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO active_alerts\n (alert_id, kind, severity, status, agent_id, message, value, threshold,\n last_seen_unix, metadata_json)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 10
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "fa09ff199445b7bf689ef4426cbd20d89040230ffff977cc1b4c73b499b1b33f"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO meta (key, value) VALUES (?1, ?2)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ffed939cfeecca1d46bdf77b67aca2919b34d496e729ad08f87ac9782fb8e382"
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
# Wakey
|
||||||
|
|
||||||
|
Wakey models devices discovered on a local network and the control-plane view used to wake and manage them across agents.
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
**Device**:
|
||||||
|
An operator-facing aggregate that represents one discovered network identity candidate.
|
||||||
|
_Avoid_: Host, machine, route
|
||||||
|
|
||||||
|
**Device Summary**:
|
||||||
|
The derived names, MACs, IPs, and presence shown on a device aggregate.
|
||||||
|
_Avoid_: Source of truth, route data
|
||||||
|
|
||||||
|
**Endpoint**:
|
||||||
|
One observed network contact point for a device, including the agent/source, optional MAC, optional IP, presence, and observation time.
|
||||||
|
_Avoid_: Raw observation, route, IP row
|
||||||
|
|
||||||
|
**Endpoint Source**:
|
||||||
|
The typed origin of an endpoint, such as neighbor data, DHCP lease data, or a hotplug hook.
|
||||||
|
_Avoid_: String kind, source label
|
||||||
|
|
||||||
|
**Observation Fact**:
|
||||||
|
Debug evidence copied from local observation inputs before it is interpreted into endpoints.
|
||||||
|
_Avoid_: Endpoint, identifier, source of truth
|
||||||
|
|
||||||
|
**Presence**:
|
||||||
|
The current reachability interpretation for a device or endpoint.
|
||||||
|
_Avoid_: Endpoint state, status
|
||||||
|
|
||||||
|
**Wake Route**:
|
||||||
|
An endpoint that has enough information and agent availability to attempt waking a device.
|
||||||
|
_Avoid_: Device, endpoint when not wakeable
|
||||||
|
|
||||||
|
**Known Device**:
|
||||||
|
A device identity manually curated by the operator.
|
||||||
|
_Avoid_: Auto-pinned device, saved observation
|
||||||
|
|
||||||
|
**Identifier**:
|
||||||
|
A manually approved MAC or IP value that links an observed device or endpoint to a known device.
|
||||||
|
_Avoid_: Observation key, route id
|
||||||
|
|
||||||
|
**Endpoint Key**:
|
||||||
|
The typed identity of endpoint evidence, based on source and optional MAC/IP.
|
||||||
|
_Avoid_: Stringly typed route id
|
||||||
|
|
||||||
|
**Agent Endpoint Key**:
|
||||||
|
An endpoint key scoped to the agent that observed it.
|
||||||
|
_Avoid_: Concatenated agent/source/MAC/IP string
|
||||||
|
|
||||||
|
**Agent**:
|
||||||
|
A fleet member that observes local network state and sends device snapshots to the control plane.
|
||||||
|
_Avoid_: Router when referring to the software actor
|
||||||
|
|
||||||
|
**Agent Host**:
|
||||||
|
The machine running an agent, including host identity and runtime metadata.
|
||||||
|
_Avoid_: Device when referring to the agent machine
|
||||||
|
|
||||||
|
**Agent Capability**:
|
||||||
|
Something an agent host can observe or do on the network.
|
||||||
|
_Avoid_: Router flag
|
||||||
|
|
||||||
|
**Interface Telemetry**:
|
||||||
|
Current operational measurements for a network interface visible to an agent.
|
||||||
|
_Avoid_: Endpoint state, device presence
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
- A **Device** has zero or more **Endpoints**.
|
||||||
|
- A **Device Summary** is derived from a **Device** and its **Endpoints**.
|
||||||
|
- Local source rows are grouped into **Devices** by endpoint-derived **DeviceId**, not by exact **Endpoint Key**.
|
||||||
|
- Within one complete local snapshot, MAC+IP endpoints may absorb IP-only endpoints with the same IP; this merge is not remembered across snapshots unless the operator creates an **Identifier**.
|
||||||
|
- An **Endpoint** belongs to exactly one **Agent**.
|
||||||
|
- An **Endpoint** has one **Endpoint Key**.
|
||||||
|
- An **Agent Endpoint Key** combines an **Agent** with an **Endpoint Key**.
|
||||||
|
- An **Endpoint** may have both MAC and IP, MAC only, or IP only.
|
||||||
|
- An **Endpoint** has exactly one **Endpoint Source**.
|
||||||
|
- An **Endpoint** has one **Presence**.
|
||||||
|
- An **Endpoint** may name the network interface where it was observed.
|
||||||
|
- An **Observation Fact** may explain why an **Endpoint** exists, but does not define identity or wakeability.
|
||||||
|
- A **Wake Route** is derived from exactly one **Endpoint**.
|
||||||
|
- A **Wake Route** may carry the interface from its source **Endpoint**.
|
||||||
|
- A **Known Device** has zero or more **Identifiers**.
|
||||||
|
- An **Identifier** can link observed **Devices** or **Endpoints** to one **Known Device**.
|
||||||
|
- An **Agent** runs on one **Agent Host**.
|
||||||
|
- An **Agent Capability** describes what an **Agent Host** can observe or do.
|
||||||
|
- **Interface Telemetry** describes an **Agent Host** interface, not a **Device Endpoint**.
|
||||||
|
|
||||||
|
## Example dialogue
|
||||||
|
|
||||||
|
> **Dev:** "This **Device** has three IPs. Which one should the UI wake?"
|
||||||
|
> **Domain expert:** "Do not infer that from the **Device** summary. Use the **Wake Routes** derived from its **Endpoints**."
|
||||||
|
|
||||||
|
## Flagged ambiguities
|
||||||
|
|
||||||
|
- "route" was used for both observed network contact points and wakeable paths — resolved: use **Endpoint** for observed contact points and **Wake Route** only when the endpoint can be used for wake.
|
||||||
|
- "device" was used for both an identity aggregate and each MAC/IP pair — resolved: use **Device** for the aggregate and **Endpoint** for each observed MAC/IP/source/state tuple.
|
||||||
|
- `Device.ips` and `Device.macs` are true summary values, but they are not authoritative for wake, freshness, or per-IP state decisions — resolved: use **Endpoint** for those decisions.
|
||||||
|
- MAC-only and IP-only observations are still **Endpoints** — resolved: **Wake Routes** require a MAC and a connected **Agent**, while IP is optional route detail.
|
||||||
|
- Endpoint-specific states such as "removed" or "stale" are not separate concepts yet — resolved: use **Presence** until a distinction appears that presence cannot express cleanly.
|
||||||
|
- Endpoint origin is not a free string — resolved: use **Endpoint Source** for domain logic and reserve raw strings for debug facts only.
|
||||||
|
- Local observation file rows map naturally to **Observation Facts** — resolved: keep them for debugging, but do not use them as the operational model.
|
||||||
|
- Offline **Endpoints** remain part of the API model — resolved: UI may collapse them from the primary view, but the API should still expose them for details, copy, and debugging.
|
||||||
|
- **Device Summary** IPs come only from concrete live-ish sources such as neighbor table rows and current DHCP lease rows — resolved: hotplug hook memory creates **Endpoints** and **Observation Facts**, but does not directly inflate summary IPs.
|
||||||
|
- Router-ness is not a boolean domain concept — resolved: model **Agent Capabilities** and **Agent Host** metadata instead.
|
||||||
|
- **Presence** ordering should treat **Unknown** as non-voting/no evidence, weaker than **Offline** — resolved: concrete offline evidence should not be overridden by unknown hook or DHCP ambiguity.
|
||||||
|
- **Device** presence is derived from all **Endpoints**, not only summary-eligible endpoints.
|
||||||
|
- Current DHCP leases are summary-eligible **Endpoints** with **Unknown** presence; they may support wake when a MAC is present, but they do not prove reachability.
|
||||||
|
- DHCP hook events, including removal, indicate **Unknown** because they describe lease churn rather than reachability.
|
||||||
|
- Neighbor table and neighbor hook events may indicate **Offline** when they report failed or removed reachability.
|
||||||
|
- **Presence** and wakeability are separate: an **Offline** endpoint may still produce a **Wake Route** if it has a MAC and a connected **Agent**.
|
||||||
|
- **Endpoint** does not store wakeability; wakeability is derived from endpoint data plus **Agent** runtime and capability context.
|
||||||
|
- Preferred **Wake Routes** are ordered by connected agent, wakeability, having an IP, presence quality, recency, then source quality.
|
||||||
|
- **Endpoint** evidence preserves source-specific rows; equivalent **Wake Routes** may ignore source when they target the same agent, MAC, and IP.
|
||||||
|
- The control plane stores **Endpoints** as structured data so fleet search, identity matching, and wake route selection do not depend on parsing opaque device JSON.
|
||||||
|
- Summary MAC/IP collections are derived from **Endpoints**; separate MAC/IP storage should be removed or represented as views rather than maintained as independent truth.
|
||||||
|
- API arrays should be domain-sorted by default; UI may sort top-level fleet rows for presentation, but route ranking and recommended route selection are backend-owned.
|
||||||
|
- The main fleet API embeds **Endpoints** in each **Device** response; a separate endpoint API is unnecessary until payload size or access patterns require it.
|
||||||
|
- The control plane stores current per-agent endpoint snapshots, not endpoint event history.
|
||||||
|
- Agent-local hook memory is retained by local last-seen age policy, not by control-plane acknowledgement; default retention is 30 days unless configured otherwise.
|
||||||
|
- Live-source **Endpoints** use snapshot time as their observed time; hook-derived **Endpoints** use the hook row's recorded last-seen time.
|
||||||
|
- **DeviceId**, **Endpoint Key**, and **Agent Endpoint Key** are typed concepts; string keys are boundary serialization only.
|
||||||
|
- **DeviceId** is derived from a device's **Endpoints**: canonical MAC if any endpoint has a MAC, otherwise canonical IP, otherwise absent.
|
||||||
|
- Hostnames are endpoint evidence/display data, not part of **Endpoint Key** identity.
|
||||||
|
- Interface names are endpoint metadata, not part of **Endpoint Key** identity; multi-segment routers may require scoped endpoint identity later if the same source/MAC/IP has different meaning on different interfaces.
|
||||||
|
- An **Endpoint Key** must contain at least one network address value: MAC, IP, or both. Facts without MAC or IP remain **Observation Facts** only.
|
||||||
|
- **Identifiers** are source-independent ownership claims over MAC or permanent IP values, not claims over endpoint sources.
|
||||||
|
- IP **Identifiers** are explicit operator claims; Wakey does not infer whether an IP is permanent, static, or reserved.
|
||||||
Generated
+762
-111
File diff suppressed because it is too large.
Load diff
+17
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey"
|
name = "wakey"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
@@ -32,6 +32,22 @@ strip = true
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["ipjs", "wakey-agent", "wakey-control-plane", "wakey-core", "wakey-linux"]
|
members = ["ipjs", "wakey-agent", "wakey-control-plane", "wakey-core", "wakey-linux"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
macaddr = { version = "1", features = ["serde", "serde_std"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = [
|
||||||
|
"fs",
|
||||||
|
"process",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"io-util",
|
||||||
|
"macros",
|
||||||
|
"time",
|
||||||
|
] }
|
||||||
|
tracing = "0.1"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
[dependencies.lda-ipjs]
|
[dependencies.lda-ipjs]
|
||||||
path = "ipjs"
|
path = "ipjs"
|
||||||
registry = "gitea"
|
registry = "gitea"
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Example:
|
|||||||
data_dir = "/var/lib/wakey-control-plane"
|
data_dir = "/var/lib/wakey-control-plane"
|
||||||
bind = "0.0.0.0:8080"
|
bind = "0.0.0.0:8080"
|
||||||
public_url = "https://cp.example.com"
|
public_url = "https://cp.example.com"
|
||||||
state_file = "state.db"
|
state_file = "state.sqlite3"
|
||||||
pid_file = "wakey-control-plane.pid"
|
pid_file = "wakey-control-plane.pid"
|
||||||
ui_dist_dir = "/opt/wakey/ui/dist"
|
ui_dist_dir = "/opt/wakey/ui/dist"
|
||||||
command_timeout_ms = 30000
|
command_timeout_ms = 30000
|
||||||
@@ -140,9 +140,14 @@ json_logs = false
|
|||||||
If `telemetry.otlp_endpoint` is omitted, logs still work normally and only local
|
If `telemetry.otlp_endpoint` is omitted, logs still work normally and only local
|
||||||
structured logs are emitted.
|
structured logs are emitted.
|
||||||
|
|
||||||
State is persisted in an embedded `sled` database (default
|
State is persisted in an embedded SQLite database (default
|
||||||
`/var/lib/wakey-control-plane/state.db`).
|
`/var/lib/wakey-control-plane/state.sqlite3`).
|
||||||
Relative paths in config are resolved under `data_dir`.
|
Relative paths in config are resolved under `data_dir`.
|
||||||
|
Legacy sled state can be imported explicitly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey-control-plane migrate-sqlite-state --from /var/lib/wakey-control-plane/legacy.sqlite3 --to /var/lib/wakey-control-plane/state.sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
Enroll tokens are now expiring and revocable. Issuance returns `expires_at_unix`.
|
Enroll tokens are now expiring and revocable. Issuance returns `expires_at_unix`.
|
||||||
Expired tokens are rejected on enroll and can be garbage-collected periodically
|
Expired tokens are rejected on enroll and can be garbage-collected periodically
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# lda rambling
|
||||||
|
|
||||||
|
currently the collection of fleetwakeroutes doesnt make much sense.
|
||||||
|
|
||||||
|
it goes deep. Read code, then this again.
|
||||||
|
|
||||||
|
## big fleet shows everything.
|
||||||
|
|
||||||
|
all the IPs, offline or not. I at least want to hide the offline/unknown IPs into a drawer somewhere. its too cluttered.
|
||||||
|
|
||||||
|
i do need offline IPs, because, a failed / dhcp-rotated IP may be of use. but what use? copying only. You cant even send shit to it, its FAILED.
|
||||||
|
|
||||||
|
so not all the IPs should be immediately invisible.
|
||||||
|
|
||||||
|
## big fleet DOESNT show everything
|
||||||
|
|
||||||
|
yea showing all the offline IP but only ONE online ip per mac for wake routes.
|
||||||
|
|
||||||
|
BRING This BACK. i need ALL the routes. Color coded (already has that with disabled wake button)
|
||||||
|
|
||||||
|
## big collect collects everything
|
||||||
|
|
||||||
|
at wakey, collection makes no sense; a device saves all IPs without saying which of them is not real.
|
||||||
|
|
||||||
|
oh you do have to search again for the observations because they are unknown ahh hints,
|
||||||
|
|
||||||
|
## logics are misleading
|
||||||
|
|
||||||
|
also IPs without macs are unknown, (should be OFFLINE? ips without macs? what do you think it is)
|
||||||
|
while macs without ips are offline (pulled straight from the Big Mac Name cache)
|
||||||
|
|
||||||
|
what is this ahh logic
|
||||||
|
|
||||||
|
## struct names are misleading
|
||||||
|
|
||||||
|
Big Device over here, collected (fresh AND old) From the router ITSELF. Is BY MAC.
|
||||||
|
|
||||||
|
but the Device struct was designed to take multiple MACs, Why. This is very misleading.
|
||||||
|
|
||||||
|
The migration was from Observations, which is a string fest, to Device, which
|
||||||
|
|
||||||
|
1. give up the very cool observations based design, which fundamentally is just tracking identifing pairs of (ip, mac, maybe hostname)
|
||||||
|
2. doesnt solve the confusing ahh mac-keyed Devices. Why is the deviceid optional? What!
|
||||||
|
|
||||||
|
## big code needs a rewrite
|
||||||
|
|
||||||
|
i think we need to have a very clear model. And such model is ALREADY PRESENT! fleet wake route MIGHT be the coolest thing ever, solves everything.
|
||||||
|
|
||||||
|
Should type instead of free string construction. For example. alot of the keys are currently freehanded. You need a struct with Display or ToString or a dedicated method.
|
||||||
|
hopefully those keys can have a helper contructor. HOPEFULLY NOONE PULLS INFO OUT OF THE STRING BY SLICING IT.
|
||||||
|
|
||||||
|
If you want to keep just device.macs, device.ips, you can, we need to collect the Wake routes! which goes deep,
|
||||||
|
|
||||||
|
but is reasonable, because there are two sources that give us identifiers, neighbors and observations. We flatten this to device, at the router its keyed by mac or failed ip.
|
||||||
|
At the fleet idfk, i dont control that part of the code; its all AI.
|
||||||
|
|
||||||
|
So, if we can collect wake targets directly from device. Saves a whole plane of problems. suddenly wakey-cc is like 6 million times slimmer, because this is a wakey or wakey-core problem.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Core Endpoints and Wake Routes
|
||||||
|
|
||||||
|
Wakey will model observed network contact points as **Endpoints** in `wakey-core`, and derive **Wake Routes** from those endpoints instead of reconstructing routes from flattened device summary MAC/IP arrays. `Device.ips` and `Device.macs` remain true summary fields for operator display and compatibility, but endpoint data is authoritative for wake, freshness, source, per-IP state, and route selection.
|
||||||
|
|
||||||
|
**Status**: accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The previous model stored a `Device` aggregate with summary `macs`, `ips`, `presence`, and raw observation facts. The control plane then built fleet wake routes from combinations like first MAC plus first IP. That lost source, presence, interface, and MAC/IP-pair evidence, especially when hook memory contained removed IPs or when several sources described the same address differently.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add a typed `DeviceEndpoint` concept in `wakey-core`. A device snapshot sent by an agent contains devices with endpoints already attached. The control plane stores structured endpoint rows and derives fleet summaries and wake routes from those rows.
|
||||||
|
|
||||||
|
Endpoint shape:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
DeviceEndpoint {
|
||||||
|
key: EndpointKey,
|
||||||
|
hostname: Option<String>,
|
||||||
|
interface: Option<String>,
|
||||||
|
presence: Presence,
|
||||||
|
first_seen_unix: Option<u64>,
|
||||||
|
last_seen_unix: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
EndpointKey {
|
||||||
|
source: EndpointSource,
|
||||||
|
mac: Option<MacAddr>,
|
||||||
|
ip: Option<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentEndpointKey {
|
||||||
|
agent_id: String,
|
||||||
|
endpoint: EndpointKey,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`EndpointKey` must contain at least one of MAC or IP. Hostname and interface are endpoint evidence/metadata, not identity. `AgentEndpointKey` may use `serde(flatten)` for wire shape, but hash values are never durable storage keys. String keys are boundary serialization only.
|
||||||
|
|
||||||
|
## Source and Presence Rules
|
||||||
|
|
||||||
|
Endpoint sources are typed domain variants, not free strings:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Neighbor
|
||||||
|
DhcpLease
|
||||||
|
HookNeighbor
|
||||||
|
HookDhcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Presence answers “is this reachable?”, not “can this be woken?” The presence order is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Unknown < Offline < LikelyOnline < Online
|
||||||
|
```
|
||||||
|
|
||||||
|
Source mapping:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Neighbor reachable/permanent -> Online
|
||||||
|
Neighbor stale -> LikelyOnline
|
||||||
|
Neighbor failed/remove-like evidence -> Offline
|
||||||
|
Other neighbor states -> Unknown
|
||||||
|
|
||||||
|
Current DHCP lease -> Unknown
|
||||||
|
Hook DHCP add/update/remove -> Unknown
|
||||||
|
Hook neighbor add/update/old -> LikelyOnline
|
||||||
|
Hook neighbor remove -> Offline
|
||||||
|
```
|
||||||
|
|
||||||
|
Current DHCP leases are summary-eligible and may support wake if they include a MAC, but they do not prove reachability. Hook memory creates endpoints and observation facts, but does not directly inflate summary IPs.
|
||||||
|
|
||||||
|
## Summary, Presence, and UI
|
||||||
|
|
||||||
|
`Device.ips` and `Device.macs` stay as summaries. Summary IPs come from concrete live-ish sources such as current neighbor rows and current DHCP lease rows. Hook-derived and offline IPs remain visible through endpoints but are collapsed out of primary UI summaries by default.
|
||||||
|
|
||||||
|
`Device.presence` is derived from all endpoints, not only summary-eligible endpoints. This lets concrete offline evidence matter while allowing stronger live evidence to override it.
|
||||||
|
|
||||||
|
The main fleet API embeds endpoints inside each fleet device. API responses expose all endpoints, including offline and unknown endpoints. The UI may collapse offline/unknown endpoints in the primary row, but details must expose them for copy/debug/history.
|
||||||
|
|
||||||
|
## Wakeability and Wake Route Ordering
|
||||||
|
|
||||||
|
Wakeability is derived, not stored on `DeviceEndpoint`.
|
||||||
|
|
||||||
|
An endpoint is wakeable when:
|
||||||
|
|
||||||
|
```text
|
||||||
|
endpoint has a MAC
|
||||||
|
agent is connected
|
||||||
|
agent has wake capability
|
||||||
|
endpoint source is allowed for wake route derivation
|
||||||
|
```
|
||||||
|
|
||||||
|
For now, a connected wakey-agent is assumed to have wake capability. Presence does not gate wakeability: an `Offline` endpoint may still produce a wake route, because waking powered-off devices is the core use case.
|
||||||
|
|
||||||
|
Preferred wake route order:
|
||||||
|
|
||||||
|
```text
|
||||||
|
connected agent
|
||||||
|
wakeable route
|
||||||
|
has IP
|
||||||
|
presence quality
|
||||||
|
newest last_seen
|
||||||
|
source quality
|
||||||
|
```
|
||||||
|
|
||||||
|
Presence quality for route ranking follows:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Online > LikelyOnline > Offline > Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
Source quality follows:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Neighbor > DhcpLease > HookNeighbor > HookDhcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalent wake routes may ignore endpoint source when they target the same agent, MAC, and IP. Endpoint evidence preserves source-specific rows; route grouping may deduplicate equivalent targets.
|
||||||
|
|
||||||
|
## Device and Identity Rules
|
||||||
|
|
||||||
|
`DeviceId` remains the typed observed identity enum:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
DeviceId::Mac(MacAddr)
|
||||||
|
DeviceId::Ip(IpAddr)
|
||||||
|
```
|
||||||
|
|
||||||
|
`DeviceId` is derived from endpoints: canonical MAC if any endpoint has a MAC, otherwise canonical IP, otherwise absent. Local source rows are grouped into devices by endpoint-derived `DeviceId`, not exact `EndpointKey`.
|
||||||
|
|
||||||
|
Within a single complete local snapshot, a MAC+IP endpoint may absorb an IP-only endpoint with the same IP. This merge is not remembered across snapshots unless the operator creates a known-device identifier.
|
||||||
|
|
||||||
|
Known-device identifiers are source-independent ownership claims over MAC or permanent IP values. Wakey does not infer whether an IP is permanent, static, or reserved; the UI should warn the operator when attaching an IP identifier.
|
||||||
|
|
||||||
|
## Control Plane Storage
|
||||||
|
|
||||||
|
The control plane stores current per-agent endpoint snapshots as structured columns, not opaque `Device` JSON and not endpoint event history. Summary MAC/IP collections are derived from endpoints; separate MAC/IP storage should be removed or represented as views rather than maintained as independent truth.
|
||||||
|
|
||||||
|
Agent-local hook memory is retained by local last-seen age policy, not by control-plane acknowledgement. The default retention is 30 days unless configured otherwise. Live-source endpoints use snapshot time as their observed time; hook-derived endpoints use the hook row's recorded last-seen time.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Wake route construction moves away from `macs.first()` / `ips.first()` guessing.
|
||||||
|
- The agent/core layer owns source interpretation because it still has local context.
|
||||||
|
- The control plane can search, filter, match known identifiers, and rank wake routes from structured endpoint rows.
|
||||||
|
- UI can show clean summaries while keeping complete endpoint evidence in details.
|
||||||
|
- This preserves “wake an offline machine by MAC” without pretending that offline means not wakeable.
|
||||||
+13
-1
@@ -7,6 +7,8 @@ param(
|
|||||||
|
|
||||||
$PASSWD = Get-DefaultPassword $PASSWD
|
$PASSWD = Get-DefaultPassword $PASSWD
|
||||||
|
|
||||||
|
Write-Output $PSScriptRoot
|
||||||
|
|
||||||
# idk what this does it works like that then thats how it is
|
# idk what this does it works like that then thats how it is
|
||||||
pscp.exe -l root -scp -pw $PASSWD -r 192.168.100.1:/etc/ldlda_help $PSScriptRoot
|
pscp.exe -l root -scp -pw $PASSWD -r 192.168.100.1:/etc/ldlda_help $PSScriptRoot
|
||||||
pscp.exe -l root -scp -pw $PASSWD 192.168.100.1:/etc/rc.local $PSScriptRoot
|
pscp.exe -l root -scp -pw $PASSWD 192.168.100.1:/etc/rc.local $PSScriptRoot
|
||||||
@@ -23,5 +25,15 @@ New-Item -ItemType Directory -Force -Path $initDir | Out-Null
|
|||||||
$files = "update_wakey" , "wakey" , "update_tailscale" , "wireguard_setup", "lda-override"
|
$files = "update_wakey" , "wakey" , "update_tailscale" , "wireguard_setup", "lda-override"
|
||||||
$remote = $files.ForEach({ "/etc/init.d/$_" })
|
$remote = $files.ForEach({ "/etc/init.d/$_" })
|
||||||
$remote | ForEach-Object {
|
$remote | ForEach-Object {
|
||||||
pscp.exe -l root -scp -pw $PASSWD "192.168.100.1:$_" "$initDir\"
|
pscp.exe -l root -scp -pw $PASSWD "192.168.100.1:$_" "$initDir/"
|
||||||
|
}
|
||||||
|
|
||||||
|
$hotplugDir = Join-Path $PSScriptRoot 'hotplug.d'
|
||||||
|
New-Item -ItemType Directory -Force -Path $hotplugDir | Out-Null
|
||||||
|
|
||||||
|
$files = "dhcp/95-wakey", "neigh/95-wakey"
|
||||||
|
|
||||||
|
$files | ForEach-Object {
|
||||||
|
New-Item -ItemType Directory -Force -Path (Split-Path "$hotplugDir/$_")
|
||||||
|
pscp.exe -l root -scp -pw $PASSWD "192.168.100.1:/etc/hotplug.d/$_" "$hotplugDir/$_"
|
||||||
}
|
}
|
||||||
@@ -68,8 +68,12 @@ try {
|
|||||||
$initDestTmp = "$User@${HostName}:$initTmp"
|
$initDestTmp = "$User@${HostName}:$initTmp"
|
||||||
$localDeploy = Join-Path $repoRoot 'scripts/remote_deploy_wakey.sh'
|
$localDeploy = Join-Path $repoRoot 'scripts/remote_deploy_wakey.sh'
|
||||||
$localInit = Join-Path $repoRoot 'scripts/init/openwrt/wakey'
|
$localInit = Join-Path $repoRoot 'scripts/init/openwrt/wakey'
|
||||||
|
$localDhcpHotplug = Join-Path $repoRoot 'scripts/hotplug/openwrt/dhcp/95-wakey'
|
||||||
|
$localNeighHotplug = Join-Path $repoRoot 'scripts/hotplug/openwrt/neigh/95-wakey'
|
||||||
$deployTmp = '/var/tmp/remote_deploy_wakey.sh'
|
$deployTmp = '/var/tmp/remote_deploy_wakey.sh'
|
||||||
$deployPreferred = '/root/.bin/remote_deploy_wakey.sh'
|
$deployPreferred = '/root/.bin/remote_deploy_wakey.sh'
|
||||||
|
$dhcpHotplugTmp = '/var/tmp/wakey.hotplug.dhcp.tmp'
|
||||||
|
$neighHotplugTmp = '/var/tmp/wakey.hotplug.neigh.tmp'
|
||||||
|
|
||||||
# Push binaries
|
# Push binaries
|
||||||
Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
|
Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
|
||||||
@@ -79,6 +83,12 @@ try {
|
|||||||
if (-not $SkipInitScript -and (Test-Path $localInit)) {
|
if (-not $SkipInitScript -and (Test-Path $localInit)) {
|
||||||
Invoke-Scp -Local $localInit -Dest $initDestTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
|
Invoke-Scp -Local $localInit -Dest $initDestTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
|
||||||
}
|
}
|
||||||
|
if (Test-Path $localDhcpHotplug) {
|
||||||
|
Invoke-Scp -Local $localDhcpHotplug -Dest "$User@${HostName}:$dhcpHotplugTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
|
||||||
|
}
|
||||||
|
if (Test-Path $localNeighHotplug) {
|
||||||
|
Invoke-Scp -Local $localNeighHotplug -Dest "$User@${HostName}:$neighHotplugTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
|
||||||
|
}
|
||||||
|
|
||||||
# Push deploy helper if exists
|
# Push deploy helper if exists
|
||||||
if (Test-Path $localDeploy) {
|
if (Test-Path $localDeploy) {
|
||||||
@@ -103,6 +113,16 @@ if [ -f $initTmp ]; then
|
|||||||
fi
|
fi
|
||||||
"@
|
"@
|
||||||
})
|
})
|
||||||
|
if [ -f $dhcpHotplugTmp ]; then
|
||||||
|
sed -i "s/\r$//" $dhcpHotplugTmp 2>/dev/null || true
|
||||||
|
mkdir -p /etc/hotplug.d/dhcp
|
||||||
|
cp -f $dhcpHotplugTmp /etc/hotplug.d/dhcp/95-wakey
|
||||||
|
fi
|
||||||
|
if [ -f $neighHotplugTmp ]; then
|
||||||
|
sed -i "s/\r$//" $neighHotplugTmp 2>/dev/null || true
|
||||||
|
mkdir -p /etc/hotplug.d/neigh
|
||||||
|
cp -f $neighHotplugTmp /etc/hotplug.d/neigh/95-wakey
|
||||||
|
fi
|
||||||
"@
|
"@
|
||||||
$remoteCmd = "sh -lc '$($script -replace "`r",'')'"
|
$remoteCmd = "sh -lc '$($script -replace "`r",'')'"
|
||||||
if ($Quiet) { $remoteCmd += " >/dev/null 2>&1" }
|
if ($Quiet) { $remoteCmd += " >/dev/null 2>&1" }
|
||||||
|
|||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
BIN="/root/.bin/wakey-agent"
|
||||||
|
[ -x "$BIN" ] || BIN="$(command -v wakey-agent 2>/dev/null)"
|
||||||
|
[ -n "$BIN" ] || exit 0
|
||||||
|
[ -n "$ACTION" ] || exit 0
|
||||||
|
[ -n "$MACADDR" ] || exit 0
|
||||||
|
|
||||||
|
run_wakey_agent() {
|
||||||
|
if [ -n "$WAKEY_AGENT_CONFIG" ]; then
|
||||||
|
set -- --config "$WAKEY_AGENT_CONFIG" "$@"
|
||||||
|
fi
|
||||||
|
if [ "$WAKEY_HOTPLUG_VERBOSE" = "1" ]; then
|
||||||
|
set -- -v "$@"
|
||||||
|
fi
|
||||||
|
# [ "$WAKEY_HOTPLUG_LOG" = "1" ] &&
|
||||||
|
if command -v logger >/dev/null 2>&1; then
|
||||||
|
"$BIN" "$@" 2>&1 | logger -t wakey-hotplug
|
||||||
|
else
|
||||||
|
"$BIN" "$@" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$IPADDR" ] && [ -n "$HOSTNAME" ]; then
|
||||||
|
run_wakey_agent observe dhcp --action "$ACTION" --mac "$MACADDR" --ip "$IPADDR" --hostname "$HOSTNAME" || true
|
||||||
|
elif [ -n "$IPADDR" ]; then
|
||||||
|
run_wakey_agent observe dhcp --action "$ACTION" --mac "$MACADDR" --ip "$IPADDR" || true
|
||||||
|
elif [ -n "$HOSTNAME" ]; then
|
||||||
|
run_wakey_agent observe dhcp --action "$ACTION" --mac "$MACADDR" --hostname "$HOSTNAME" || true
|
||||||
|
else
|
||||||
|
run_wakey_agent observe dhcp --action "$ACTION" --mac "$MACADDR" || true
|
||||||
|
fi
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
BIN="/root/.bin/wakey-agent"
|
||||||
|
[ -x "$BIN" ] || BIN="$(command -v wakey-agent 2>/dev/null)"
|
||||||
|
[ -n "$BIN" ] || exit 0
|
||||||
|
[ -n "$ACTION" ] || exit 0
|
||||||
|
|
||||||
|
run_wakey_agent() {
|
||||||
|
if [ -n "$WAKEY_AGENT_CONFIG" ]; then
|
||||||
|
set -- --config "$WAKEY_AGENT_CONFIG" "$@"
|
||||||
|
fi
|
||||||
|
if [ "$WAKEY_HOTPLUG_VERBOSE" = "1" ]; then
|
||||||
|
set -- -v "$@"
|
||||||
|
fi
|
||||||
|
# [ "$WAKEY_HOTPLUG_LOG" = "1" ] &&
|
||||||
|
if command -v logger >/dev/null 2>&1; then
|
||||||
|
"$BIN" "$@" 2>&1 | logger -t wakey-hotplug
|
||||||
|
else
|
||||||
|
"$BIN" "$@" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$MACADDR" ] && [ -n "$IPADDR" ]; then
|
||||||
|
run_wakey_agent observe neigh --action "$ACTION" --mac "$MACADDR" --ip "$IPADDR" || true
|
||||||
|
elif [ -n "$MACADDR" ]; then
|
||||||
|
run_wakey_agent observe neigh --action "$ACTION" --mac "$MACADDR" || true
|
||||||
|
elif [ -n "$IPADDR" ]; then
|
||||||
|
run_wakey_agent observe neigh --action "$ACTION" --ip "$IPADDR" || true
|
||||||
|
else
|
||||||
|
run_wakey_agent observe neigh --action "$ACTION" || true
|
||||||
|
fi
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -104,8 +104,10 @@ main() {
|
|||||||
# Ensure execute bits on staged files we know should be executable
|
# Ensure execute bits on staged files we know should be executable
|
||||||
for f in \
|
for f in \
|
||||||
"$STAGING/etc/init.d/"* \
|
"$STAGING/etc/init.d/"* \
|
||||||
|
"$STAGING/etc/hotplug.d/"*/* \
|
||||||
"$STAGING/etc/ldlda_help/"*.sh \
|
"$STAGING/etc/ldlda_help/"*.sh \
|
||||||
"$STAGING/root/.bin/wakey" \
|
"$STAGING/root/.bin/wakey" \
|
||||||
|
"$STAGING/root/.bin/wakey-agent" \
|
||||||
"$STAGING/root/.bin/kill_wakey.sh" \
|
"$STAGING/root/.bin/kill_wakey.sh" \
|
||||||
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
|
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
|
||||||
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
|
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
|
||||||
@@ -114,11 +116,14 @@ main() {
|
|||||||
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
|
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
|
||||||
for f in \
|
for f in \
|
||||||
"$STAGING/etc/init.d/"* \
|
"$STAGING/etc/init.d/"* \
|
||||||
|
"$STAGING/etc/hotplug.d/"*/* \
|
||||||
"$STAGING/etc/ldlda_help/"*.sh \
|
"$STAGING/etc/ldlda_help/"*.sh \
|
||||||
"$STAGING/root/.bin/"*.sh; do
|
"$STAGING/root/.bin/"*.sh; do
|
||||||
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
|
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
|
|
||||||
|
chown -R root:root "$STAGING" 2>/dev/null || true
|
||||||
|
|
||||||
# Copy staged tree into /
|
# Copy staged tree into /
|
||||||
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
|
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,24 @@ Get-ChildItem (Join-Path $root 'scripts/init/openwrt') -File | ForEach-Object {
|
|||||||
Set-Content -NoNewline -LiteralPath $dest -Value $content -Encoding UTF8
|
Set-Content -NoNewline -LiteralPath $dest -Value $content -Encoding UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Copy OpenWrt hotplug hooks.
|
||||||
|
$hotplugSrc = Join-Path $root 'scripts/hotplug/openwrt'
|
||||||
|
if (Test-Path $hotplugSrc) {
|
||||||
|
Get-ChildItem $hotplugSrc -Directory | ForEach-Object {
|
||||||
|
$hotplugTypeDir = Join-Path $staging ("etc/hotplug.d/" + $_.Name)
|
||||||
|
New-Item -ItemType Directory -Force -Path $hotplugTypeDir | Out-Null
|
||||||
|
Get-ChildItem $_.FullName -File | ForEach-Object {
|
||||||
|
$dest = Join-Path $hotplugTypeDir $_.Name
|
||||||
|
$content = Get-Content -Raw -LiteralPath $_.FullName
|
||||||
|
$content = $content -replace "`r`n", "`n"
|
||||||
|
Set-Content -NoNewline -LiteralPath $dest -Value $content -Encoding UTF8
|
||||||
|
if (Get-Command chmod -ErrorAction SilentlyContinue) {
|
||||||
|
chmod +x $dest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Copy helper scripts intended for /etc/ldlda_help
|
# Copy helper scripts intended for /etc/ldlda_help
|
||||||
$helpSrc = Join-Path $root 'scripts/ldlda_help'
|
$helpSrc = Join-Path $root 'scripts/ldlda_help'
|
||||||
if (Test-Path $helpSrc) {
|
if (Test-Path $helpSrc) {
|
||||||
|
|||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
DB="${SQLX_PREPARE_DB:-/tmp/wakey-sqlx-prepare.sqlite3}"
|
||||||
|
DATABASE_URL="sqlite://$DB"
|
||||||
|
|
||||||
|
if ! cargo sqlx --version >/dev/null 2>&1; then
|
||||||
|
echo "cargo-sqlx is not installed." >&2
|
||||||
|
echo "Install it with: cargo install sqlx-cli --no-default-features --features sqlite" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$DB" "$DB-shm" "$DB-wal"
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
DATABASE_URL="$DATABASE_URL" cargo sqlx database create
|
||||||
|
DATABASE_URL="$DATABASE_URL" cargo sqlx migrate run --source wakey-control-plane/migrations
|
||||||
|
DATABASE_URL="$DATABASE_URL" cargo sqlx prepare --workspace -- -p wakey-control-plane --all-targets --all-features
|
||||||
|
rm -rf wakey-control-plane/.sqlx
|
||||||
|
mkdir -p wakey-control-plane/.sqlx
|
||||||
|
cp .sqlx/*.json wakey-control-plane/.sqlx/
|
||||||
|
|
||||||
|
echo "SQLx metadata prepared in $ROOT/.sqlx"
|
||||||
|
echo "SQLx package metadata mirrored in $ROOT/wakey-control-plane/.sqlx"
|
||||||
+2
-1
@@ -42,6 +42,7 @@ if ($versionProvided) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Ensure build ok
|
# Ensure build ok
|
||||||
|
$env:SQLX_OFFLINE = 'true'
|
||||||
cargo fmt
|
cargo fmt
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ if ($Publish) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($Tag -and $Version) {
|
if ($Tag -and $Version) {
|
||||||
git tag -f "v$Version"
|
git tag -f -a "v$Version" -m "Release $Version"
|
||||||
git push -f origin "v$Version"
|
git push -f origin "v$Version"
|
||||||
Write-Host "Pushed tag: v$Version"
|
Write-Host "Pushed tag: v$Version"
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,30 @@
|
|||||||
# then packaging the same bundle format and installing it.
|
# then packaging the same bundle format and installing it.
|
||||||
#
|
#
|
||||||
# Simplest use:
|
# Simplest use:
|
||||||
# sudo -E ./scripts/update_wakey_cc_from_repo.sh
|
# ./scripts/update_wakey_cc_from_repo.sh
|
||||||
#
|
#
|
||||||
# Env options:
|
# Env options:
|
||||||
|
# WAKEY_CC_REPO_DIR Existing checkout to update/build instead of cloning
|
||||||
# WAKEY_CC_REPO_URL Git URL (default: https://git.ldlda.com/lda/wakey.git)
|
# WAKEY_CC_REPO_URL Git URL (default: https://git.ldlda.com/lda/wakey.git)
|
||||||
# WAKEY_CC_REF Git ref/branch/tag (default: main)
|
# WAKEY_CC_REF Git ref/branch/tag (default: main)
|
||||||
# WAKEY_CC_TARGET Rust target triple (default from uname)
|
# WAKEY_CC_TARGET Rust target triple (default from uname)
|
||||||
# WAKEY_CC_ROOT Install root (default: current directory)
|
# WAKEY_CC_ROOT Install root (default: current directory)
|
||||||
# WAKEY_CC_SERVICE systemd unit name (default: wakey-cc.service)
|
# WAKEY_CC_SERVICE systemd unit name (default: wakey-cc.service)
|
||||||
|
# WAKEY_CC_USER Owner for installed files/temp dirs (default: current user)
|
||||||
# WAKEY_CC_VERSION Bundle version label (default: v0.0.0-local-<short_sha>)
|
# WAKEY_CC_VERSION Bundle version label (default: v0.0.0-local-<short_sha>)
|
||||||
# WAKEY_CC_NO_RESTART If set, skip systemd restart
|
# WAKEY_CC_NO_RESTART If set, skip systemd restart
|
||||||
# WAKEY_CC_KEEP_TMP If set, keep temp checkout/build dir for debugging
|
# WAKEY_CC_KEEP_TMP If set, keep temp checkout/build dir for debugging
|
||||||
# SUDO Privilege escalation command/path (default: sudo when needed)
|
# SUDO Privilege escalation command/path for install steps (default: sudo)
|
||||||
#
|
#
|
||||||
# Requires: git, cargo, tar, pnpm, systemctl (optional for restart)
|
# Requires: git, cargo, tar, pnpm, systemctl (optional for restart)
|
||||||
|
|
||||||
|
# Common existing-checkout use:
|
||||||
|
: <<'USE CASE'
|
||||||
|
WAKEY_CC_REPO_DIR="$HOME/dev/wakey" \
|
||||||
|
WAKEY_CC_USER=lda \
|
||||||
|
WAKEY_CC_KEEP_TMP=1 \
|
||||||
|
scripts/update_wakey_cc_from_repo.sh
|
||||||
|
USE CASE
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
log() { printf '[update_wakey_cc_from_repo] %s\n' "$*"; }
|
log() { printf '[update_wakey_cc_from_repo] %s\n' "$*"; }
|
||||||
@@ -64,14 +73,83 @@ sudo_cmd() {
|
|||||||
fail 'root privileges are required for install/restart steps; set SUDO or run as root'
|
fail 'root privileges are required for install/restart steps; set SUDO or run as root'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
priv() {
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
$SUDO_BIN "$@"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_label() {
|
||||||
|
path=$1
|
||||||
|
if command -v restorecon >/dev/null 2>&1; then
|
||||||
|
priv restorecon "$path" || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
chown_if_requested() {
|
||||||
|
[ -n "${OWNER:-}" ] || return 0
|
||||||
|
priv chown -R "$OWNER:$OWNER" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
chmod_user_rw_if_requested() {
|
||||||
|
[ -n "${OWNER:-}" ] || return 0
|
||||||
|
priv chmod -R u+rwX "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_existing_repo() {
|
||||||
|
repo_dir=$1
|
||||||
|
ref=$2
|
||||||
|
|
||||||
|
[ -d "$repo_dir/.git" ] || fail "WAKEY_CC_REPO_DIR is not a git checkout: $repo_dir"
|
||||||
|
|
||||||
|
log "using existing checkout $repo_dir (ref: $ref)"
|
||||||
|
git -C "$repo_dir" fetch --prune origin "$ref" || fail 'git fetch failed'
|
||||||
|
git -C "$repo_dir" checkout "$ref" || fail "git checkout failed: $ref"
|
||||||
|
git -C "$repo_dir" pull --ff-only origin "$ref" || fail 'git pull --ff-only failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
install_executable_force() {
|
||||||
|
src=$1
|
||||||
|
dst=$2
|
||||||
|
dir=$(dirname "$dst")
|
||||||
|
tmp="$dst.new.$$"
|
||||||
|
old="$dst.old.$$"
|
||||||
|
|
||||||
|
priv mkdir -p "$dir"
|
||||||
|
priv rm -f "$tmp"
|
||||||
|
priv cp -f "$src" "$tmp"
|
||||||
|
priv chmod 0755 "$tmp"
|
||||||
|
chown_if_requested "$tmp"
|
||||||
|
if [ -e "$dst" ]; then
|
||||||
|
priv mv -f "$dst" "$old" || true
|
||||||
|
fi
|
||||||
|
priv mv -f "$tmp" "$dst"
|
||||||
|
priv rm -f "$old"
|
||||||
|
restore_label "$dst"
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
REPO_DIR="${WAKEY_CC_REPO_DIR:-}"
|
||||||
REPO_URL="${WAKEY_CC_REPO_URL:-https://git.ldlda.com/lda/wakey.git}"
|
REPO_URL="${WAKEY_CC_REPO_URL:-https://git.ldlda.com/lda/wakey.git}"
|
||||||
REF="${WAKEY_CC_REF:-main}"
|
REF="${WAKEY_CC_REF:-main}"
|
||||||
TARGET="${WAKEY_CC_TARGET:-$(default_target)}"
|
TARGET="${WAKEY_CC_TARGET:-$(default_target)}"
|
||||||
ROOT="${WAKEY_CC_ROOT:-$PWD}"
|
ROOT="${WAKEY_CC_ROOT:-$PWD}"
|
||||||
SERVICE="${WAKEY_CC_SERVICE:-wakey-cc.service}"
|
SERVICE="${WAKEY_CC_SERVICE:-wakey-cc.service}"
|
||||||
|
if [ -n "${WAKEY_CC_USER:-}" ]; then
|
||||||
|
OWNER="$WAKEY_CC_USER"
|
||||||
|
elif [ "$(id -u)" -eq 0 ]; then
|
||||||
|
OWNER=
|
||||||
|
else
|
||||||
|
OWNER=$(id -un)
|
||||||
|
fi
|
||||||
SUDO_BIN=$(sudo_cmd)
|
SUDO_BIN=$(sudo_cmd)
|
||||||
|
|
||||||
|
if [ -n "$OWNER" ] && ! id "$OWNER" >/dev/null 2>&1; then
|
||||||
|
fail "WAKEY_CC_USER does not exist: $OWNER"
|
||||||
|
fi
|
||||||
|
|
||||||
require_cmd git
|
require_cmd git
|
||||||
require_cmd cargo
|
require_cmd cargo
|
||||||
require_cmd tar
|
require_cmd tar
|
||||||
@@ -80,19 +158,34 @@ main() {
|
|||||||
TMPBASE="${TMPDIR:-/tmp}"
|
TMPBASE="${TMPDIR:-/tmp}"
|
||||||
WORKDIR="$TMPBASE/wakey-cc-src.$$"
|
WORKDIR="$TMPBASE/wakey-cc-src.$$"
|
||||||
STAGING="$TMPBASE/wakey-cc-stage.$$"
|
STAGING="$TMPBASE/wakey-cc-stage.$$"
|
||||||
|
CLONED_WORKDIR=1
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [ -z "${WAKEY_CC_KEEP_TMP:-}" ]; then
|
if [ -z "${WAKEY_CC_KEEP_TMP:-}" ]; then
|
||||||
rm -rf "$WORKDIR" "$STAGING"
|
[ "$CLONED_WORKDIR" = "0" ] || rm -rf "$WORKDIR"
|
||||||
|
rm -rf "$STAGING"
|
||||||
else
|
else
|
||||||
log "keeping temp dir: $WORKDIR"
|
if [ "$CLONED_WORKDIR" != "0" ]; then
|
||||||
|
chown_if_requested "$WORKDIR"
|
||||||
|
chmod_user_rw_if_requested "$WORKDIR"
|
||||||
|
log "keeping temp dir: $WORKDIR"
|
||||||
|
fi
|
||||||
|
chown_if_requested "$STAGING"
|
||||||
|
chmod_user_rw_if_requested "$STAGING"
|
||||||
|
log "keeping temp dir: $STAGING"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
log "cloning $REPO_URL (ref: $REF)"
|
if [ -n "$REPO_DIR" ]; then
|
||||||
git clone --depth 1 --branch "$REF" "$REPO_URL" "$WORKDIR" ||
|
WORKDIR=$REPO_DIR
|
||||||
fail 'git clone failed'
|
CLONED_WORKDIR=0
|
||||||
|
update_existing_repo "$WORKDIR" "$REF"
|
||||||
|
else
|
||||||
|
log "cloning $REPO_URL (ref: $REF)"
|
||||||
|
git clone --depth 1 --branch "$REF" "$REPO_URL" "$WORKDIR" ||
|
||||||
|
fail 'git clone failed'
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$WORKDIR"
|
cd "$WORKDIR"
|
||||||
|
|
||||||
@@ -124,26 +217,29 @@ main() {
|
|||||||
[ -f "$STAGING/bin/wakey-control-plane" ] || fail 'bundle missing bin/wakey-control-plane'
|
[ -f "$STAGING/bin/wakey-control-plane" ] || fail 'bundle missing bin/wakey-control-plane'
|
||||||
[ -f "$STAGING/ui/dist/index.html" ] || fail 'bundle missing ui/dist/index.html'
|
[ -f "$STAGING/ui/dist/index.html" ] || fail 'bundle missing ui/dist/index.html'
|
||||||
|
|
||||||
if [ "$(id -u)" -ne 0 ] && [ -n "$SUDO_BIN" ]; then
|
priv mkdir -p "$ROOT"
|
||||||
$SUDO_BIN mkdir -p "$ROOT"
|
install_executable_force \
|
||||||
$SUDO_BIN cp -a "$STAGING/." "$ROOT/"
|
"$STAGING/bin/wakey-control-plane" \
|
||||||
else
|
"$ROOT/bin/wakey-control-plane"
|
||||||
mkdir -p "$ROOT"
|
priv cp -a "$STAGING/ui" "$ROOT/"
|
||||||
cp -a "$STAGING/." "$ROOT/"
|
priv cp -a "$STAGING/scripts" "$ROOT/"
|
||||||
fi
|
priv cp -a "$STAGING/deploy" "$ROOT/"
|
||||||
|
chown_if_requested \
|
||||||
|
"$ROOT/bin/wakey-control-plane" \
|
||||||
|
"$ROOT/ui" \
|
||||||
|
"$ROOT/scripts" \
|
||||||
|
"$ROOT/deploy"
|
||||||
|
chmod_user_rw_if_requested \
|
||||||
|
"$ROOT/ui" \
|
||||||
|
"$ROOT/scripts" \
|
||||||
|
"$ROOT/deploy"
|
||||||
|
|
||||||
if [ -z "${WAKEY_CC_NO_RESTART:-}" ] && command -v systemctl >/dev/null 2>&1; then
|
if [ -z "${WAKEY_CC_NO_RESTART:-}" ] && command -v systemctl >/dev/null 2>&1; then
|
||||||
if systemctl list-unit-files "$SERVICE" >/dev/null 2>&1; then
|
if systemctl list-unit-files "$SERVICE" >/dev/null 2>&1; then
|
||||||
log "restarting $SERVICE"
|
log "restarting $SERVICE"
|
||||||
if [ "$(id -u)" -ne 0 ] && [ -n "$SUDO_BIN" ]; then
|
priv systemctl daemon-reload
|
||||||
$SUDO_BIN systemctl daemon-reload
|
priv systemctl restart "$SERVICE"
|
||||||
$SUDO_BIN systemctl restart "$SERVICE"
|
priv systemctl --no-pager --full status "$SERVICE" | sed -n '1,16p'
|
||||||
$SUDO_BIN systemctl --no-pager --full status "$SERVICE" | sed -n '1,16p'
|
|
||||||
else
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart "$SERVICE"
|
|
||||||
systemctl --no-pager --full status "$SERVICE" | sed -n '1,16p'
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
log "service $SERVICE not installed; skipped restart"
|
log "service $SERVICE not installed; skipped restart"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+71
-12
@@ -25,7 +25,7 @@ pub struct Cli {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Show merged device inventory rows.
|
/// Show merged device inventory rows.
|
||||||
#[command(visible_alias = "status")]
|
#[command(visible_alias = "status", alias = "inv")]
|
||||||
Inventory(InventoryArgs),
|
Inventory(InventoryArgs),
|
||||||
/// Show DHCP leases, optionally enriched with current neighbor state.
|
/// Show DHCP leases, optionally enriched with current neighbor state.
|
||||||
Leases(LeasesArgs),
|
Leases(LeasesArgs),
|
||||||
@@ -33,15 +33,20 @@ pub enum Command {
|
|||||||
Wake(WakeArgs),
|
Wake(WakeArgs),
|
||||||
/// Show condensed network interface summaries.
|
/// Show condensed network interface summaries.
|
||||||
Devs(DevsArgs),
|
Devs(DevsArgs),
|
||||||
|
/// Record local hotplug observations.
|
||||||
|
Observe(ObserveArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct LeasesArgs {
|
pub struct LeasesArgs {
|
||||||
/// Include best-known current neighbor state for each lease IP.
|
/// Legacy: include neighbor state on lease rows. Prefer `wakey inventory`.
|
||||||
#[arg(long)]
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Deprecated: include neighbor state on lease rows. Prefer `wakey inventory`."
|
||||||
|
)]
|
||||||
pub include_state: bool,
|
pub include_state: bool,
|
||||||
/// Print machine-readable JSON instead of a table.
|
/// Print machine-readable JSON instead of a table.
|
||||||
#[arg(long)]
|
#[arg(long, short)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +70,7 @@ pub struct WakeArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub ip: Option<IpAddr>,
|
pub ip: Option<IpAddr>,
|
||||||
/// Print machine-readable JSON instead of a table.
|
/// Print machine-readable JSON instead of a table.
|
||||||
#[arg(long)]
|
#[arg(short, long)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +100,7 @@ pub struct InventoryArgs {
|
|||||||
#[arg(long = "mac")]
|
#[arg(long = "mac")]
|
||||||
pub macs: Vec<macaddr::MacAddr>,
|
pub macs: Vec<macaddr::MacAddr>,
|
||||||
/// Print machine-readable JSON instead of a table.
|
/// Print machine-readable JSON instead of a table.
|
||||||
#[arg(long)]
|
#[arg(short, long)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +114,49 @@ pub struct DevsArgs {
|
|||||||
/// Optional interface name to show.
|
/// Optional interface name to show.
|
||||||
pub dev: Option<String>,
|
pub dev: Option<String>,
|
||||||
/// Show only interfaces whose operstate is `up`.
|
/// Show only interfaces whose operstate is `up`.
|
||||||
#[arg(long)]
|
#[arg(short, long)]
|
||||||
pub up: bool,
|
pub up: bool,
|
||||||
/// Print machine-readable JSON instead of a table.
|
/// Print machine-readable JSON instead of a table.
|
||||||
#[arg(long)]
|
#[arg(short, long)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ObserveArgs {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: ObserveCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum ObserveCommand {
|
||||||
|
/// Observe a dnsmasq DHCP lease event.
|
||||||
|
Dhcp(ObserveDhcpArgs),
|
||||||
|
/// Observe a neighbor-table event.
|
||||||
|
Neigh(ObserveNeighArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ObserveDhcpArgs {
|
||||||
|
#[arg(long)]
|
||||||
|
pub action: String,
|
||||||
|
#[arg(long)]
|
||||||
|
pub mac: macaddr::MacAddr,
|
||||||
|
#[arg(long)]
|
||||||
|
pub ip: Option<IpAddr>,
|
||||||
|
#[arg(long)]
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ObserveNeighArgs {
|
||||||
|
#[arg(long)]
|
||||||
|
pub action: String,
|
||||||
|
#[arg(long)]
|
||||||
|
pub mac: Option<macaddr::MacAddr>,
|
||||||
|
#[arg(long)]
|
||||||
|
pub ip: Option<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_tracing(verbose: u8) {
|
pub fn init_tracing(verbose: u8) {
|
||||||
let filter = EnvFilter::try_from_default_env()
|
let filter = EnvFilter::try_from_default_env()
|
||||||
.or_else(|_| EnvFilter::try_new(default_filter_for_verbosity(verbose)))
|
.or_else(|_| EnvFilter::try_new(default_filter_for_verbosity(verbose)))
|
||||||
@@ -163,10 +204,11 @@ pub async fn run(cli: Cli) -> Result<()> {
|
|||||||
json = args.json,
|
json = args.json,
|
||||||
"dispatching leases command"
|
"dispatching leases command"
|
||||||
);
|
);
|
||||||
let leases = wakey::get_leases(wakey_core::LeaseQuery {
|
let leases = if args.include_state {
|
||||||
include_state: args.include_state,
|
wakey::get_leases_with_neighbor_state().await?
|
||||||
})
|
} else {
|
||||||
.await?;
|
wakey::get_leases().await?
|
||||||
|
};
|
||||||
if args.json {
|
if args.json {
|
||||||
println!("{}", serde_json::to_string_pretty(&leases)?);
|
println!("{}", serde_json::to_string_pretty(&leases)?);
|
||||||
} else {
|
} else {
|
||||||
@@ -206,6 +248,23 @@ pub async fn run(cli: Cli) -> Result<()> {
|
|||||||
println!("{}", table::render_devs_table(&devs));
|
println!("{}", table::render_devs_table(&devs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Command::Observe(args) => match args.command {
|
||||||
|
ObserveCommand::Dhcp(args) => {
|
||||||
|
let changed = wakey::observe_dhcp_event(
|
||||||
|
&args.action,
|
||||||
|
args.mac,
|
||||||
|
args.ip,
|
||||||
|
args.hostname.as_deref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
println!("observed=dhcp changed={changed}");
|
||||||
|
}
|
||||||
|
ObserveCommand::Neigh(args) => {
|
||||||
|
let changed =
|
||||||
|
wakey::observe_neighbor_event(&args.action, args.mac, args.ip).await?;
|
||||||
|
println!("observed=neigh changed={changed}");
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+7
-1
@@ -3,12 +3,18 @@
|
|||||||
|
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub use wakey_linux;
|
||||||
|
|
||||||
pub use service::{
|
pub use service::{
|
||||||
broadcast_wake_targets, get_interface_summaries, get_interface_summary, get_ips, get_leases,
|
broadcast_wake_targets, get_interface_summaries, get_interface_summary, get_ips, get_leases,
|
||||||
inventory, leases_without_state, merge_devices, resolve_devices, resolve_query,
|
get_leases_with_neighbor_state, inventory, leases_without_state, local_observation_to_fact,
|
||||||
|
merge_devices, merge_devices_with_observations, resolve_devices, resolve_query,
|
||||||
resolve_selector, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
|
resolve_selector, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
|
||||||
};
|
};
|
||||||
|
pub use wakey_linux::observations::{
|
||||||
|
list_local_observations, observe_dhcp_event, observe_neighbor_event,
|
||||||
|
prune_stale_observations_from_path,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
+237
-34
@@ -1,7 +1,8 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use tracing::{debug, warn};
|
||||||
use wakey_core::{
|
use wakey_core::{
|
||||||
Device, DeviceInventory, DhcpLease, DhcpLeaseWithState, InventoryQuery, NeighborEntry,
|
Device, DeviceInventory, DeviceObservationFact, DhcpLease, DhcpLeaseWithState, InventoryQuery,
|
||||||
Presence, Query,
|
NeighborEntry, Presence, Query,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::service::leases::get_leases;
|
use crate::service::leases::get_leases;
|
||||||
@@ -20,13 +21,26 @@ pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
|
|||||||
/// directly from raw Linux source rows.
|
/// directly from raw Linux source rows.
|
||||||
pub async fn inventory(query: InventoryQuery) -> Result<DeviceInventory> {
|
pub async fn inventory(query: InventoryQuery) -> Result<DeviceInventory> {
|
||||||
let neighbors = wakey_linux::devices::query_neighbors(&query).await?;
|
let neighbors = wakey_linux::devices::query_neighbors(&query).await?;
|
||||||
let leases = get_leases(wakey_core::LeaseQuery {
|
let leases = get_leases().await?;
|
||||||
include_state: false,
|
let observations = match wakey_linux::observations::list_local_observations().await {
|
||||||
})
|
Ok(observations) => observations
|
||||||
.await?;
|
.into_iter()
|
||||||
Ok(DeviceInventory {
|
.filter_map(local_observation_to_fact)
|
||||||
devices: merge_devices(neighbors, leases, &query),
|
.collect::<Vec<_>>(),
|
||||||
})
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed reading local hook observations for inventory");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!(
|
||||||
|
neighbors = neighbors.len(),
|
||||||
|
leases = leases.len(),
|
||||||
|
observations = observations.len(),
|
||||||
|
"building device inventory"
|
||||||
|
);
|
||||||
|
let devices = merge_devices_with_observations(neighbors, leases, observations, &query);
|
||||||
|
debug!(devices = devices.len(), "merged device inventory");
|
||||||
|
Ok(DeviceInventory { devices })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge raw neighbor entries and DHCP leases into device aggregates.
|
/// Merge raw neighbor entries and DHCP leases into device aggregates.
|
||||||
@@ -37,51 +51,79 @@ pub fn merge_devices(
|
|||||||
neighbors: Vec<NeighborEntry>,
|
neighbors: Vec<NeighborEntry>,
|
||||||
leases: Vec<DhcpLeaseWithState>,
|
leases: Vec<DhcpLeaseWithState>,
|
||||||
query: &InventoryQuery,
|
query: &InventoryQuery,
|
||||||
|
) -> Vec<Device> {
|
||||||
|
merge_devices_with_observations(neighbors, leases, Vec::new(), query)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge raw neighbor entries, DHCP leases, and hook observations into device
|
||||||
|
/// aggregates.
|
||||||
|
pub fn merge_devices_with_observations(
|
||||||
|
neighbors: Vec<NeighborEntry>,
|
||||||
|
leases: Vec<DhcpLeaseWithState>,
|
||||||
|
observations: Vec<DeviceObservationFact>,
|
||||||
|
query: &InventoryQuery,
|
||||||
) -> Vec<Device> {
|
) -> Vec<Device> {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
let mut by_mac: BTreeMap<String, (Vec<NeighborEntry>, Vec<DhcpLease>)> = BTreeMap::new();
|
type DeviceParts = (
|
||||||
|
Vec<NeighborEntry>,
|
||||||
|
Vec<DhcpLease>,
|
||||||
|
Vec<DeviceObservationFact>,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut by_key: BTreeMap<String, DeviceParts> = BTreeMap::new();
|
||||||
|
|
||||||
for row in neighbors {
|
for row in neighbors {
|
||||||
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_mac.entry(key).or_default().0.push(row);
|
by_key.entry(key).or_default().0.push(row);
|
||||||
}
|
}
|
||||||
for lease in leases {
|
for lease in leases {
|
||||||
let key = lease.lease_line.mac.to_string();
|
let key = lease.lease_line.mac.to_string();
|
||||||
by_mac.entry(key).or_default().1.push(lease.lease_line);
|
by_key.entry(key).or_default().1.push(lease.lease_line);
|
||||||
|
}
|
||||||
|
for observation in observations {
|
||||||
|
let key = observation
|
||||||
|
.mac
|
||||||
|
.map(|mac| mac.to_string())
|
||||||
|
.or_else(|| observation.ip.map(|ip| format!("ip:{ip}")))
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
by_key.entry(key).or_default().2.push(observation);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut devices: Vec<Device> = by_mac
|
let mut devices: Vec<Device> = by_key
|
||||||
.into_values()
|
.into_values()
|
||||||
.map(|(neighbors, leases)| Device::from_parts(neighbors, leases))
|
.map(|(neighbors, leases, observations)| {
|
||||||
|
Device::from_parts_with_observations(neighbors, leases, observations)
|
||||||
|
})
|
||||||
.collect();
|
.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();
|
for term in query {
|
||||||
let mut devs = Vec::new();
|
match term {
|
||||||
let mut ips = Vec::new();
|
Query::Text(v) => texts.push(v.as_str()),
|
||||||
let mut macs = Vec::new();
|
Query::Interface(v) => devs.push(v.as_str()),
|
||||||
let mut nuds = Vec::new();
|
Query::Ip(v) => ips.push(*v),
|
||||||
|
Query::Mac(v) => macs.push(*v),
|
||||||
|
Query::NeighborState(v) => nuds.push(*v),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for term in query {
|
devices.retain(|device| {
|
||||||
match term {
|
(texts.is_empty() || device.names.iter().any(|n| texts.contains(&n.as_str())))
|
||||||
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())))
|
|
||||||
&& (devs.is_empty() || device.interfaces.iter().any(|i| devs.contains(&i.as_str()))) // same pattern as iter().any
|
&& (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)))
|
&& (ips.is_empty() || device.ips.iter().any(|ip| ips.contains(ip)))
|
||||||
&& (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))
|
||||||
@@ -90,6 +132,42 @@ pub fn merge_devices(
|
|||||||
devices
|
devices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn local_observation_to_fact(
|
||||||
|
observation: wakey_linux::observations::LocalDeviceObservation,
|
||||||
|
) -> Option<DeviceObservationFact> {
|
||||||
|
let mac = match observation.mac.as_deref() {
|
||||||
|
Some(raw) => match raw.parse() {
|
||||||
|
Ok(mac) => Some(mac),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
mac = raw,
|
||||||
|
error = %err,
|
||||||
|
"ignoring invalid observed mac in inventory"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
if mac.is_none() && observation.ip.is_none() {
|
||||||
|
warn!(
|
||||||
|
kind = %observation.kind,
|
||||||
|
action = %observation.action,
|
||||||
|
"ignoring hook observation without mac or ip"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(DeviceObservationFact {
|
||||||
|
kind: observation.kind,
|
||||||
|
action: observation.action,
|
||||||
|
mac,
|
||||||
|
ip: observation.ip,
|
||||||
|
hostname: observation.hostname,
|
||||||
|
first_seen_unix: Some(observation.first_seen_unix),
|
||||||
|
last_seen_unix: Some(observation.last_seen_unix),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const fn presence_rank(presence: Presence) -> u8 {
|
const fn presence_rank(presence: Presence) -> u8 {
|
||||||
match presence {
|
match presence {
|
||||||
Presence::Online => 3,
|
Presence::Online => 3,
|
||||||
@@ -103,7 +181,7 @@ const fn presence_rank(presence: Presence) -> u8 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use wakey_core::{DhcpLease, InventoryQueryBuilder, NeighborState};
|
use wakey_core::{DeviceId, DhcpLease, EndpointSource, InventoryQueryBuilder, NeighborState};
|
||||||
|
|
||||||
fn sample_neighbors() -> Vec<NeighborEntry> {
|
fn sample_neighbors() -> Vec<NeighborEntry> {
|
||||||
vec![NeighborEntry {
|
vec![NeighborEntry {
|
||||||
@@ -154,4 +232,129 @@ mod tests {
|
|||||||
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
|
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_uses_ip_observed_id_when_mac_is_absent() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let out = merge_devices(
|
||||||
|
vec![NeighborEntry {
|
||||||
|
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)),
|
||||||
|
dev: Some("br-lan".to_string()),
|
||||||
|
mac: None,
|
||||||
|
state: NeighborState::Stale,
|
||||||
|
}],
|
||||||
|
Vec::new(),
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
out[0].id,
|
||||||
|
Some(DeviceId::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20))))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_preserves_hook_observation_facts() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let out = merge_devices_with_observations(
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![DeviceObservationFact {
|
||||||
|
kind: "dhcp".into(),
|
||||||
|
action: "update".into(),
|
||||||
|
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||||
|
ip: Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 30))),
|
||||||
|
hostname: Some("lda".into()),
|
||||||
|
first_seen_unix: Some(10),
|
||||||
|
last_seen_unix: Some(20),
|
||||||
|
}],
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].names, vec!["lda".to_string()]);
|
||||||
|
assert_eq!(
|
||||||
|
out[0].id,
|
||||||
|
Some(DeviceId::Mac("aa:bb:cc:dd:ee:ff".parse().expect("mac")))
|
||||||
|
);
|
||||||
|
assert_eq!(out[0].observations.len(), 1);
|
||||||
|
assert_eq!(out[0].endpoints.len(), 1);
|
||||||
|
assert_eq!(out[0].endpoints[0].key.source, EndpointSource::HookDhcp);
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
|
assert_eq!(out[0].presence, Presence::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_marks_neigh_remove_observation_offline() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let out = merge_devices_with_observations(
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![DeviceObservationFact {
|
||||||
|
kind: "neigh".into(),
|
||||||
|
action: "remove".into(),
|
||||||
|
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||||
|
ip: Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 30))),
|
||||||
|
hostname: None,
|
||||||
|
first_seen_unix: Some(10),
|
||||||
|
last_seen_unix: Some(20),
|
||||||
|
}],
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].presence, Presence::Offline);
|
||||||
|
assert_eq!(out[0].endpoints[0].key.source, EndpointSource::HookNeighbor);
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_keeps_dhcp_remove_observation_unknown() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let out = merge_devices_with_observations(
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![DeviceObservationFact {
|
||||||
|
kind: "dhcp".into(),
|
||||||
|
action: "remove".into(),
|
||||||
|
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||||
|
ip: Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 30))),
|
||||||
|
hostname: None,
|
||||||
|
first_seen_unix: Some(10),
|
||||||
|
last_seen_unix: Some(20),
|
||||||
|
}],
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].presence, Presence::Unknown);
|
||||||
|
assert_eq!(out[0].endpoints[0].key.source, EndpointSource::HookDhcp);
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_uses_ip_id_for_ip_only_hook_endpoint_without_summary_ip() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31));
|
||||||
|
let out = merge_devices_with_observations(
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![DeviceObservationFact {
|
||||||
|
kind: "neigh".into(),
|
||||||
|
action: "remove".into(),
|
||||||
|
mac: None,
|
||||||
|
ip: Some(ip),
|
||||||
|
hostname: None,
|
||||||
|
first_seen_unix: Some(10),
|
||||||
|
last_seen_unix: Some(20),
|
||||||
|
}],
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].id, Some(DeviceId::Ip(ip)));
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
|
assert_eq!(out[0].endpoints.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+16
-8
@@ -1,16 +1,24 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use wakey_core::{DhcpLease, DhcpLeaseWithState, LeaseQuery};
|
use wakey_core::{DhcpLease, DhcpLeaseWithState};
|
||||||
|
|
||||||
/// Read DHCP leases and optionally enrich them with current neighbor-state data.
|
/// Read DHCP leases from dnsmasq plus remembered hook names.
|
||||||
pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
|
pub async fn get_leases() -> Result<Vec<DhcpLeaseWithState>> {
|
||||||
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
|
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
|
||||||
.await
|
.await
|
||||||
.context("failed to read DHCP leases")?;
|
.context("failed to read DHCP leases")?;
|
||||||
if query.include_state {
|
Ok(leases_without_state(leases))
|
||||||
Ok(wakey_linux::dhcp::enrich_leases_with_nud_state(leases).await)
|
}
|
||||||
} else {
|
|
||||||
Ok(leases_without_state(leases))
|
/// Read DHCP leases and enrich them with current neighbor state.
|
||||||
}
|
///
|
||||||
|
/// Prefer inventory for device status; this exists for the legacy leases view.
|
||||||
|
pub async fn get_leases_with_neighbor_state() -> Result<Vec<DhcpLeaseWithState>> {
|
||||||
|
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
|
||||||
|
.await
|
||||||
|
.context("failed to read DHCP leases")?;
|
||||||
|
// this is only used in the wakey CLI
|
||||||
|
#[allow(deprecated)]
|
||||||
|
Ok(wakey_linux::dhcp::enrich_leases_with_nud_state(leases).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrap raw DHCP leases in the current service output shape without neighbor state.
|
/// Wrap raw DHCP leases in the current service output shape without neighbor state.
|
||||||
|
|||||||
+5
-2
@@ -5,8 +5,11 @@ pub mod query;
|
|||||||
pub mod wake;
|
pub mod wake;
|
||||||
|
|
||||||
pub use interfaces::{get_interface_summaries, get_interface_summary, get_ips};
|
pub use interfaces::{get_interface_summaries, get_interface_summary, get_ips};
|
||||||
pub use inventory::{inventory, merge_devices, resolve_devices};
|
pub use inventory::{
|
||||||
pub use leases::{get_leases, leases_without_state};
|
inventory, local_observation_to_fact, merge_devices, merge_devices_with_observations,
|
||||||
|
resolve_devices,
|
||||||
|
};
|
||||||
|
pub use leases::{get_leases, get_leases_with_neighbor_state, leases_without_state};
|
||||||
pub use query::{resolve_query, resolve_selector};
|
pub use query::{resolve_query, resolve_selector};
|
||||||
pub use wake::{
|
pub use wake::{
|
||||||
broadcast_wake_targets, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
|
broadcast_wake_targets, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -18,11 +18,13 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-compiler-runtime": "^1.0.0",
|
"react-compiler-runtime": "^1.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"shadcn": "^4.2.0",
|
"shadcn": "^4.2.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
|
|||||||
Generated
+55
@@ -25,6 +25,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.8.0
|
specifier: ^1.8.0
|
||||||
version: 1.8.0([email protected])
|
version: 1.8.0([email protected])
|
||||||
|
next-themes:
|
||||||
|
specifier: ^0.4.6
|
||||||
|
version: 0.4.6([email protected]([email protected]))([email protected])
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@@ -40,6 +43,9 @@ importers:
|
|||||||
shadcn:
|
shadcn:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0(@types/[email protected])([email protected])
|
version: 4.2.0(@types/[email protected])([email protected])
|
||||||
|
sonner:
|
||||||
|
specifier: ^2.0.7
|
||||||
|
version: 2.0.7([email protected]([email protected]))([email protected])
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.5.0
|
specifier: ^3.5.0
|
||||||
version: 3.5.0
|
version: 3.5.0
|
||||||
@@ -843,6 +849,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -851,6 +858,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -859,6 +867,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -867,6 +876,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -875,6 +885,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -883,6 +894,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -891,6 +903,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -899,6 +912,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -907,6 +921,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -915,6 +930,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -923,6 +939,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -931,6 +948,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -939,6 +957,7 @@ packages:
|
|||||||
}
|
}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@rollup/[email protected]":
|
"@rollup/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -1060,6 +1079,7 @@ packages:
|
|||||||
engines: { node: ">= 20" }
|
engines: { node: ">= 20" }
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@tailwindcss/[email protected]":
|
"@tailwindcss/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -1069,6 +1089,7 @@ packages:
|
|||||||
engines: { node: ">= 20" }
|
engines: { node: ">= 20" }
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@tailwindcss/[email protected]":
|
"@tailwindcss/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -1078,6 +1099,7 @@ packages:
|
|||||||
engines: { node: ">= 20" }
|
engines: { node: ">= 20" }
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
"@tailwindcss/[email protected]":
|
"@tailwindcss/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -1087,6 +1109,7 @@ packages:
|
|||||||
engines: { node: ">= 20" }
|
engines: { node: ">= 20" }
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
"@tailwindcss/[email protected]":
|
"@tailwindcss/[email protected]":
|
||||||
resolution:
|
resolution:
|
||||||
@@ -2365,6 +2388,7 @@ packages:
|
|||||||
engines: { node: ">= 12.0.0" }
|
engines: { node: ">= 12.0.0" }
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution:
|
resolution:
|
||||||
@@ -2374,6 +2398,7 @@ packages:
|
|||||||
engines: { node: ">= 12.0.0" }
|
engines: { node: ">= 12.0.0" }
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution:
|
resolution:
|
||||||
@@ -2383,6 +2408,7 @@ packages:
|
|||||||
engines: { node: ">= 12.0.0" }
|
engines: { node: ">= 12.0.0" }
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution:
|
resolution:
|
||||||
@@ -2392,6 +2418,7 @@ packages:
|
|||||||
engines: { node: ">= 12.0.0" }
|
engines: { node: ">= 12.0.0" }
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution:
|
resolution:
|
||||||
@@ -2581,6 +2608,15 @@ packages:
|
|||||||
}
|
}
|
||||||
engines: { node: ">= 0.6" }
|
engines: { node: ">= 0.6" }
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==,
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -3086,6 +3122,15 @@ packages:
|
|||||||
integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==,
|
integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==,
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -4862,6 +4907,11 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]([email protected]([email protected]))([email protected]):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1([email protected])
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -5221,6 +5271,11 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]([email protected]([email protected]))([email protected]):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1([email protected])
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|||||||
+27
-4
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type Agent,
|
type Agent,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
revokeAgent,
|
revokeAgent,
|
||||||
setAgentNickname,
|
setAgentNickname,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { AppLayout } from "@/layout/AppLayout";
|
import { AppLayout } from "@/layout/AppLayout";
|
||||||
import { AgentsPage } from "@/pages/AgentsPage";
|
import { AgentsPage } from "@/pages/AgentsPage";
|
||||||
import { AlertsPage } from "@/pages/AlertsPage";
|
import { AlertsPage } from "@/pages/AlertsPage";
|
||||||
@@ -21,6 +23,7 @@ import { CommandsPage } from "@/pages/CommandsPage";
|
|||||||
import { DashboardPage } from "@/pages/DashboardPage";
|
import { DashboardPage } from "@/pages/DashboardPage";
|
||||||
import { DevicesPage } from "@/pages/DevicesPage";
|
import { DevicesPage } from "@/pages/DevicesPage";
|
||||||
import { TokensPage } from "@/pages/TokensPage";
|
import { TokensPage } from "@/pages/TokensPage";
|
||||||
|
import { WakeToolsPage } from "@/pages/WakeToolsPage";
|
||||||
|
|
||||||
type LoadState = "idle" | "loading" | "ready" | "error";
|
type LoadState = "idle" | "loading" | "ready" | "error";
|
||||||
|
|
||||||
@@ -32,11 +35,9 @@ export function App() {
|
|||||||
const [selectedAgentId, setSelectedAgentId] = useState("");
|
const [selectedAgentId, setSelectedAgentId] = useState("");
|
||||||
|
|
||||||
const [state, setState] = useState<LoadState>("idle");
|
const [state, setState] = useState<LoadState>("idle");
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
setState("loading");
|
setState("loading");
|
||||||
setError("");
|
|
||||||
try {
|
try {
|
||||||
const [nextAgents, nextAlerts, nextHistory, nextAudit] =
|
const [nextAgents, nextAlerts, nextHistory, nextAudit] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -64,13 +65,16 @@ export function App() {
|
|||||||
setState("ready");
|
setState("ready");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setState("error");
|
setState("error");
|
||||||
setError(String(err));
|
toast.error("Failed to load data", { description: String(err) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRevokeAgent(agentId: string): Promise<boolean> {
|
async function onRevokeAgent(agentId: string): Promise<boolean> {
|
||||||
const result = await revokeAgent(agentId);
|
const result = await revokeAgent(agentId);
|
||||||
await loadAll();
|
await loadAll();
|
||||||
|
if (result.revoked) {
|
||||||
|
toast.success(`Revoked ${agentId}`);
|
||||||
|
}
|
||||||
return result.revoked;
|
return result.revoked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +84,11 @@ export function App() {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const result = await setAgentNickname(agentId, nickname);
|
const result = await setAgentNickname(agentId, nickname);
|
||||||
await loadAll();
|
await loadAll();
|
||||||
|
if (result.updated) {
|
||||||
|
toast.success(`Nickname updated for ${agentId}`, {
|
||||||
|
description: nickname || "(cleared)",
|
||||||
|
});
|
||||||
|
}
|
||||||
return result.updated;
|
return result.updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +134,6 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && <pre className="error">{error}</pre>}
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<AppLayout />}>
|
<Route path="/" element={<AppLayout />}>
|
||||||
<Route
|
<Route
|
||||||
@@ -136,6 +144,18 @@ export function App() {
|
|||||||
selectedAgentId={selectedAgentId}
|
selectedAgentId={selectedAgentId}
|
||||||
onSelectAgent={setSelectedAgentId}
|
onSelectAgent={setSelectedAgentId}
|
||||||
onAfterWake={loadAll}
|
onAfterWake={loadAll}
|
||||||
|
onRefresh={loadAll}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="wake"
|
||||||
|
element={
|
||||||
|
<WakeToolsPage
|
||||||
|
agents={agents}
|
||||||
|
selectedAgentId={selectedAgentId}
|
||||||
|
onSelectAgent={setSelectedAgentId}
|
||||||
|
onAfterWake={loadAll}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -146,6 +166,7 @@ export function App() {
|
|||||||
agents={agents}
|
agents={agents}
|
||||||
alerts={alerts}
|
alerts={alerts}
|
||||||
transitions={history}
|
transitions={history}
|
||||||
|
audit={audit}
|
||||||
loading={state === "loading"}
|
loading={state === "loading"}
|
||||||
onRefresh={loadAll}
|
onRefresh={loadAll}
|
||||||
/>
|
/>
|
||||||
@@ -194,9 +215,11 @@ export function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="tokens" element={<TokensPage />} />
|
<Route path="tokens" element={<TokensPage />} />
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
<Toaster richColors position="bottom-right" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+226
-5
@@ -72,6 +72,106 @@ export type SetAgentNicknameResponse = {
|
|||||||
updated: boolean;
|
updated: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeviceIdentifier = {
|
||||||
|
identifier_key: string;
|
||||||
|
device_id: string;
|
||||||
|
kind: string;
|
||||||
|
value: string;
|
||||||
|
created_at_unix: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KnownDevice = {
|
||||||
|
device_id: string;
|
||||||
|
display_name: string;
|
||||||
|
pinned: boolean;
|
||||||
|
created_at_unix: number;
|
||||||
|
updated_at_unix: number;
|
||||||
|
notes: string | null;
|
||||||
|
identifiers: DeviceIdentifier[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KnownDeviceSummary = {
|
||||||
|
device_id: string;
|
||||||
|
display_name: string;
|
||||||
|
pinned: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FleetDeviceAgent = {
|
||||||
|
agent_id: string;
|
||||||
|
nickname?: string | null;
|
||||||
|
connected: boolean;
|
||||||
|
last_seen_unix: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FleetWakeRoute = {
|
||||||
|
route_id: string;
|
||||||
|
agent_id: string;
|
||||||
|
nickname?: string | null;
|
||||||
|
connected: boolean;
|
||||||
|
mac: string | null;
|
||||||
|
ip: string | null;
|
||||||
|
hostname: string | null;
|
||||||
|
source: string;
|
||||||
|
last_seen_unix: number;
|
||||||
|
wakeable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FleetDevice = {
|
||||||
|
device_key: string;
|
||||||
|
display_name: string;
|
||||||
|
known_device: KnownDeviceSummary | null;
|
||||||
|
pinned: boolean;
|
||||||
|
ips: string[];
|
||||||
|
macs: string[];
|
||||||
|
hostnames: string[];
|
||||||
|
agents: FleetDeviceAgent[];
|
||||||
|
sources: string[];
|
||||||
|
first_seen_unix: number | null;
|
||||||
|
last_seen_unix: number | null;
|
||||||
|
presence: string;
|
||||||
|
route_candidates: FleetWakeRoute[];
|
||||||
|
recommended_route: FleetWakeRoute | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RefreshFleetDevicesResponse = {
|
||||||
|
total_accepted: number;
|
||||||
|
agents: {
|
||||||
|
agent_id: string;
|
||||||
|
status: string;
|
||||||
|
accepted: number;
|
||||||
|
error: string | null;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WakeFleetDeviceResponse = {
|
||||||
|
route: FleetWakeRoute;
|
||||||
|
command: {
|
||||||
|
request_id: string;
|
||||||
|
status: string;
|
||||||
|
result?: unknown;
|
||||||
|
error?: { code: string; message: string; retryable?: boolean | null };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface APIErrorResponse {
|
||||||
|
error: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code: string,
|
||||||
|
public details: any,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "APIError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
...init,
|
...init,
|
||||||
@@ -83,11 +183,22 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
let detail = raw;
|
let parsed: APIErrorResponse | null = null;
|
||||||
try {
|
try {
|
||||||
detail = JSON.stringify(JSON.parse(raw), null, 2);
|
parsed = JSON.parse(raw);
|
||||||
} catch {}
|
// assume APIError
|
||||||
throw new Error(`${res.status} ${res.statusText}\n${detail}`);
|
if (parsed?.error?.details) {
|
||||||
|
parsed.error.details = JSON.stringify(parsed.error.details);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError(
|
||||||
|
`${res.status} ${res.statusText}\n${raw}`,
|
||||||
|
parsed?.error?.code ?? "unknown",
|
||||||
|
parsed?.error?.details ?? parsed?.error?.message ?? raw,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json() as Promise<T>;
|
return res.json() as Promise<T>;
|
||||||
@@ -154,6 +265,116 @@ export function setAgentNickname(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchKnownDevices(): Promise<KnownDevice[]> {
|
||||||
|
return request<KnownDevice[]>("/api/v1/control/devices");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchFleetDevices(opts?: {
|
||||||
|
query?: string;
|
||||||
|
presence?: string;
|
||||||
|
known?: string;
|
||||||
|
agentId?: string;
|
||||||
|
visibility?: "operator" | "all";
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<FleetDevice[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (opts?.query) params.set("query", opts.query);
|
||||||
|
if (opts?.presence && opts.presence !== "all") {
|
||||||
|
params.set("presence", opts.presence);
|
||||||
|
}
|
||||||
|
if (opts?.known && opts.known !== "all") params.set("known", opts.known);
|
||||||
|
if (opts?.agentId) params.set("agent_id", opts.agentId);
|
||||||
|
if (opts?.visibility) params.set("visibility", opts.visibility);
|
||||||
|
params.set("limit", String(opts?.limit ?? 500));
|
||||||
|
return request<FleetDevice[]>(
|
||||||
|
`/api/v1/control/fleet/devices?${params.toString()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshFleetDevices(input?: {
|
||||||
|
agentIds?: string[];
|
||||||
|
timeoutMs?: number;
|
||||||
|
}): Promise<RefreshFleetDevicesResponse> {
|
||||||
|
return request<RefreshFleetDevicesResponse>(
|
||||||
|
"/api/v1/control/fleet/devices/refresh",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
agent_ids: input?.agentIds ?? [],
|
||||||
|
timeout_ms: input?.timeoutMs ?? null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wakeFleetDevice(input: {
|
||||||
|
deviceKey: string;
|
||||||
|
routeId?: string | null;
|
||||||
|
timeoutMs?: number;
|
||||||
|
}): Promise<WakeFleetDeviceResponse> {
|
||||||
|
return request<WakeFleetDeviceResponse>("/api/v1/control/fleet/wake", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
device_key: input.deviceKey,
|
||||||
|
route_id: input.routeId ?? null,
|
||||||
|
timeout_ms: input.timeoutMs ?? null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createKnownDevice(input: {
|
||||||
|
display_name: string;
|
||||||
|
pinned?: boolean;
|
||||||
|
notes?: string | null;
|
||||||
|
identifiers?: { kind: string; value: string }[];
|
||||||
|
}): Promise<KnownDevice> {
|
||||||
|
return request<KnownDevice>("/api/v1/control/devices", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
display_name: input.display_name,
|
||||||
|
pinned: input.pinned ?? true,
|
||||||
|
notes: input.notes ?? null,
|
||||||
|
identifiers: input.identifiers ?? [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachDeviceIdentifier(
|
||||||
|
deviceId: string,
|
||||||
|
identifier: { kind: string; value: string },
|
||||||
|
): Promise<KnownDevice> {
|
||||||
|
return request<KnownDevice>(
|
||||||
|
`/api/v1/control/devices/${encodeURIComponent(deviceId)}/identifiers`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(identifier),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detachDeviceIdentifier(
|
||||||
|
deviceId: string,
|
||||||
|
identifierKey: string,
|
||||||
|
): Promise<KnownDevice> {
|
||||||
|
return request<KnownDevice>(
|
||||||
|
`/api/v1/control/devices/${encodeURIComponent(deviceId)}/identifiers/${encodeURIComponent(identifierKey)}`,
|
||||||
|
{ method: "DELETE" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeKnownDevice(
|
||||||
|
targetDeviceId: string,
|
||||||
|
sourceDeviceId: string,
|
||||||
|
): Promise<KnownDevice> {
|
||||||
|
return request<KnownDevice>(
|
||||||
|
`/api/v1/control/devices/${encodeURIComponent(targetDeviceId)}/merge`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ source_device_id: sourceDeviceId }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function runCommand(
|
export function runCommand(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
kind: CommandKind,
|
kind: CommandKind,
|
||||||
@@ -177,7 +398,7 @@ function buildCommandPayload(
|
|||||||
return { command: { kind: "devs", dev: null, up_only: false } };
|
return { command: { kind: "devs", dev: null, up_only: false } };
|
||||||
}
|
}
|
||||||
if (kind === "leases") {
|
if (kind === "leases") {
|
||||||
return { command: { kind: "leases", include_state: true } };
|
return { command: { kind: "leases", include_state: false } };
|
||||||
}
|
}
|
||||||
if (kind === "inventory") {
|
if (kind === "inventory") {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Agent } from "@/api";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
agents: Agent[];
|
||||||
|
value: string;
|
||||||
|
onChange: (agentId: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function displayAgentLabel(agent: Agent): string {
|
||||||
|
const nickname = agent.nickname?.trim();
|
||||||
|
return nickname ? nickname : agent.agent_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentSelector({
|
||||||
|
agents,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
const selected = agents.find((a) => a.agent_id === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v) onChange(v);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={className ?? "w-full min-w-0"}>
|
||||||
|
<span className="flex min-w-0 flex-1 items-center gap-2 text-start">
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={`size-2 shrink-0 rounded-full ${selected.connected ? "bg-emerald-500" : "bg-zinc-400"}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
{value
|
||||||
|
? selected
|
||||||
|
? displayAgentLabel(selected)
|
||||||
|
: value
|
||||||
|
: "Select agent"}
|
||||||
|
</span>
|
||||||
|
{selected ? (
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{selected.connected ? "connected" : "offline"}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent
|
||||||
|
className="max-w-[min(92vw,30rem)]"
|
||||||
|
alignItemWithTrigger={false}
|
||||||
|
>
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<SelectItem
|
||||||
|
key={agent.agent_id}
|
||||||
|
value={agent.agent_id}
|
||||||
|
className="pe-10"
|
||||||
|
disabled={!agent.connected}
|
||||||
|
>
|
||||||
|
<span className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`size-2 shrink-0 rounded-full ${agent.connected ? "bg-emerald-500" : "bg-zinc-400"}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
{displayAgentLabel(agent)}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{agent.connected ? "connected" : "offline"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loaded 100 of 238 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user