diff --git a/AGENTS.md b/AGENTS.md
index e0b917a..b03fcd6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,6 +39,7 @@ NetIgnite keeps its own look on top of the template:
- Label code blocks with a file name or extension from `app/data/code-icons.ts`, or a plain label
without a dot (`[Terminal]`); `tests/code-icons.test.ts` fails otherwise. Add new highlight
languages to `content.build.markdown.highlight.langs` in `nuxt.config.ts`.
-- The docs describe https://github.com/NetIgnite-project/Server and
- https://github.com/NetIgnite-project/Agent-ESP32. Verify behavior against their code, not
+- The docs describe https://github.com/NetIgnite-project/Server,
+ https://github.com/NetIgnite-project/Agent-ESP32 and
+ https://github.com/NetIgnite-project/Agent-Linux. Verify behavior against their code, not
their READMEs.
diff --git a/README.md b/README.md
index abe13c1..663f929 100644
--- a/README.md
+++ b/README.md
@@ -48,8 +48,9 @@ Docs live in `content/docs/` as markdown files. Navigation is declared in `app/d
new page by creating a `.md` file, adding it to the sidebar data and to
`public/static/utils/sitemap.xml`.
-The docs describe the [Server](https://github.com/NetIgnite-project/Server) and the
-[ESP32 agent](https://github.com/NetIgnite-project/Agent-ESP32). Keep them in sync with those
+The docs describe the [Server](https://github.com/NetIgnite-project/Server), the
+[ESP32 agent](https://github.com/NetIgnite-project/Agent-ESP32) and the
+[Linux agent](https://github.com/NetIgnite-project/Agent-Linux). Keep them in sync with those
repositories when behavior changes.
## Structure
diff --git a/app/data/docs.ts b/app/data/docs.ts
index 12262c4..5a9109f 100644
--- a/app/data/docs.ts
+++ b/app/data/docs.ts
@@ -29,7 +29,7 @@ export const docsSections: DocsSection[] = [
{
to: "/docs/how-it-works",
title: "How It Works",
- description: "Server, agents and devices, and what happens when you press Power On.",
+ description: "Server, agents and devices, and what happens when you power on a device.",
icon: "i-lucide-workflow",
},
{
@@ -52,21 +52,27 @@ export const docsSections: DocsSection[] = [
{
to: "/docs/server/configuration",
title: "Configuration",
- description: "config.json, environment variables, data paths and logging.",
+ description: "NETIGNITE_* environment variables, server settings, data paths and logging.",
icon: "i-lucide-settings",
},
{
to: "/docs/server/reverse-proxy",
title: "HTTPS & Reverse Proxy",
- description: "Serve the dashboard and the agent WebSocket over TLS.",
+ description: "Serve the dashboard, the API and the WebSockets over TLS.",
icon: "i-lucide-lock",
},
{
to: "/docs/server/users",
title: "Users & Accounts",
- description: "The initial admin account, sessions, roles and passwords.",
+ description: "The first admin, user management, sign-up, logins and API keys.",
icon: "i-lucide-users",
},
+ {
+ to: "/docs/server/upgrading",
+ title: "Upgrading from v1",
+ description: "Import your v1 data into v2 and set your agents up again.",
+ icon: "i-lucide-history",
+ },
],
},
{
@@ -81,15 +87,27 @@ export const docsSections: DocsSection[] = [
{
to: "/docs/guides/agents",
title: "Managing Agents",
- description: "Create agents and get the credentials they connect with.",
+ description: "Set up agents, use the ESP32 console and update the firmware.",
icon: "i-lucide-radio-tower",
},
{
to: "/docs/guides/devices",
- title: "Devices & Wake-on-LAN",
- description: "Add devices, power them on and check whether they are online.",
+ title: "Devices",
+ description: "Add devices, check their status, power them on and shut them down.",
icon: "i-lucide-monitor",
},
+ {
+ to: "/docs/guides/sharing",
+ title: "Sharing",
+ description: "Share devices with other users as viewer, operator or manager.",
+ icon: "i-lucide-share-2",
+ },
+ {
+ to: "/docs/guides/ssh",
+ title: "SSH",
+ description: "Shut down and reboot devices over SSH with a restricted key.",
+ icon: "i-lucide-key-square",
+ },
],
},
{
@@ -98,13 +116,19 @@ export const docsSections: DocsSection[] = [
{
to: "/docs/agents/esp32",
title: "ESP32 Agent",
- description: "Configure, build and flash the ESP32 agent firmware.",
+ description: "Web installer, serial console, updates and building the firmware.",
icon: "i-lucide-cpu",
},
+ {
+ to: "/docs/agents/linux",
+ title: "Linux Agent",
+ description: "Install, pair, host mode and Wake-on-LAN mode of the Linux agent.",
+ icon: "i-lucide-terminal",
+ },
{
to: "/docs/agents/protocol",
title: "Agent Protocol",
- description: "The WebSocket protocol between server and agents — build your own agent.",
+ description: "The WebSocket protocol v2 between server and agents — build your own agent.",
icon: "i-lucide-cable",
},
],
@@ -115,19 +139,19 @@ export const docsSections: DocsSection[] = [
{
to: "/docs/reference/api",
title: "REST API",
- description: "Every HTTP endpoint of the NetIgnite server.",
+ description: "Authentication, API keys and the endpoints of the NetIgnite server.",
icon: "i-lucide-braces",
},
{
to: "/docs/reference/troubleshooting",
title: "Troubleshooting",
- description: "Common problems with agents, wake-ups, status checks and logins.",
+ description: "Common problems with logins, agents, wake-ups, status checks and updates.",
icon: "i-lucide-life-buoy",
},
{
to: "/docs/reference/development",
title: "Development",
- description: "Repositories, local setup and contributing.",
+ description: "Repositories, branches, local setup, tests and contributing.",
icon: "i-lucide-code",
},
],
diff --git a/app/data/features.ts b/app/data/features.ts
index 89199eb..1796799 100644
--- a/app/data/features.ts
+++ b/app/data/features.ts
@@ -24,42 +24,42 @@ export const homeFeatures: Feature[] = [
icon: "i-lucide-zap",
title: "Wake-on-LAN",
description:
- "Power on devices remotely using Wake-on-LAN technology. Support for multiple protocols and network configurations.",
+ "Power on PCs, servers and NAS devices from anywhere. An agent in their network sends the Wake-on-LAN packet, so no port forwarding to your devices is needed.",
tone: "amber",
},
{
- icon: "i-lucide-shield-check",
- title: "Secure Access",
+ icon: "i-lucide-power",
+ title: "Shutdown & Reboot",
description:
- "Enterprise-grade security with encrypted connections, multi-factor authentication, and role-based access control.",
- tone: "primary",
+ "Shut down and reboot Linux machines through the NetIgnite Linux agent, or other devices over SSH with a login limited to exactly these two commands.",
+ tone: "cyan",
},
{
- icon: "i-lucide-chart-line",
- title: "Real-time Monitoring",
+ icon: "i-lucide-activity",
+ title: "Live Status",
description:
- "Monitor device status, network performance, and power consumption with real-time dashboards and alerts.",
+ "See which devices are online, checked every 20 seconds by IP address, TCP port, MAC address, SSH port or the Linux agent on the device.",
tone: "green",
},
{
- icon: "i-lucide-history",
- title: "Scheduled Actions",
+ icon: "i-lucide-users",
+ title: "Sharing & Roles",
description:
- "Automate device power management with intelligent scheduling and energy-saving policies.",
- tone: "cyan",
+ "Share devices with family or colleagues as viewer, operator or manager. They never get access to your agents or logins.",
+ tone: "primary",
},
{
icon: "i-lucide-smartphone",
- title: "Mobile App",
+ title: "Install as an App",
description:
- "PWA-Based Mobile apps for iOS and Android with full feature parity and offline capabilities.",
+ "Add the dashboard to the home screen of your iPhone or Android phone as an app (PWA) and power on devices on the go.",
tone: "red",
},
{
icon: "i-lucide-puzzle",
- title: "API Integration",
+ title: "REST API",
description:
- "Comprehensive REST API for custom integrations with your existing infrastructure and automation tools.",
+ "Automate with a REST API, API keys and an OpenAPI reference for your scripts and home automation.",
tone: "slate",
},
];
@@ -70,59 +70,82 @@ export const platformFeatures: Feature[] = [
icon: "i-lucide-zap",
title: "Wake-on-LAN Control",
description:
- "Power on offline systems remotely with structured workflows and fast response times.",
+ "Power on devices through an ESP32 or Linux agent in their network, from the dashboard, your phone or the API.",
tone: "amber",
},
{
- icon: "i-lucide-shield-check",
- title: "Secure Access",
+ icon: "i-lucide-power",
+ title: "Shutdown & Reboot",
description:
- "Protect your environment with encrypted traffic, strict authentication, and access controls.",
- tone: "primary",
+ "Turn Linux machines off or restart them through the NetIgnite Linux agent, which can do nothing else. Other devices work over SSH with a login restricted to shutdown and reboot.",
+ tone: "cyan",
},
{
- icon: "i-lucide-chart-line",
- title: "Real-time Visibility",
- description: "Track status, uptime, and health signals in real time from a single dashboard.",
+ icon: "i-lucide-activity",
+ title: "Live Status",
+ description:
+ "Status checks every 20 seconds by IPv4 or IPv6 address, TCP port, MAC address (IPv6 link-local), SSH port or the Linux agent on the device.",
tone: "green",
},
{
- icon: "i-lucide-history",
- title: "Scheduled Actions",
+ icon: "i-lucide-users",
+ title: "Sharing & Roles",
description:
- "Automate power routines for maintenance windows, daily operations, and energy savings.",
- tone: "cyan",
+ "Share a device as viewer (status), operator (also power actions) or manager (also settings). Agents and SSH logins stay yours.",
+ tone: "primary",
+ },
+ {
+ icon: "i-lucide-usb",
+ title: "Browser Install & Updates",
+ description:
+ "Install the ESP32 agent from Chrome, Edge or Firefox over USB. Over-the-air updates are checked before they're installed and roll back automatically if they fail.",
+ tone: "amber",
+ },
+ {
+ icon: "i-lucide-shield-check",
+ title: "Secure by Design",
+ description:
+ "Each agent has its own credential, stored on the server only as a hash, and only runs fixed commands: no remote shell, no file access.",
+ tone: "green",
},
{
icon: "i-lucide-smartphone",
- title: "Mobile Ready",
+ title: "Install as an App",
description:
- "Use a responsive interface and PWA support to manage critical tasks while on the move.",
+ "A responsive dashboard you can install as an app (PWA) on iOS, Android and the desktop.",
tone: "red",
},
{
icon: "i-lucide-puzzle",
- title: "API Integration",
- description: "Connect NetIgnite to your tools with a flexible REST API built for automation.",
+ title: "REST API",
+ description:
+ "Connect NetIgnite to your tools with a REST API, API keys and an OpenAPI reference.",
tone: "slate",
},
+ {
+ icon: "i-lucide-server",
+ title: "Hosted or Self-Hosted",
+ description:
+ "Use the hosted app or run your own server with Docker. All of NetIgnite is open source under the AGPL-3.0.",
+ tone: "primary",
+ },
];
/** "How It Works" steps on the landing page. */
export const setupSteps: SetupStep[] = [
{
- title: "Install Agent",
+ title: "Set Up an Agent",
description:
- "Deploy our lightweight agent on your network or use our cloud-based solution for instant setup.",
+ "Install the ESP32 agent from your browser, or the Linux agent with one command, in the network of your devices.",
},
{
- title: "Discover Devices",
+ title: "Add Your Devices",
description:
- "Automatically discover and catalog all network devices with their capabilities and current status.",
+ "Enter each device's MAC address and choose how NetIgnite checks whether it's online.",
},
{
title: "Take Control",
description:
- "Start managing your devices remotely through our intuitive dashboard or mobile app.",
+ "Power devices on, shut them down and reboot them from the dashboard, the app on your phone or the API.",
},
];
diff --git a/app/pages/features.vue b/app/pages/features.vue
index db827df..f81be1c 100644
--- a/app/pages/features.vue
+++ b/app/pages/features.vue
@@ -4,24 +4,24 @@ import { platformFeatures } from "~/data/features";
usePageSeo({
title: "Features — NetIgnite",
description:
- "Discover devices, trigger Wake-on-LAN power actions and keep your infrastructure available from anywhere with NetIgnite.",
+ "Wake-on-LAN, live status checks, shutdown and reboot, device sharing, agent updates over the network, an installable app and a REST API: what NetIgnite can do.",
});
const highlightCards = [
{
title: "Operational Highlights",
items: [
- "Centralized overview of device state and reachability",
- "Reliable Wake-on-LAN dispatch with retries and validation",
- "Role-based controls for secure team operations",
+ "One overview of all your devices and whether they're online",
+ "Wake-on-LAN from an agent in the device's own network",
+ "Per-device roles: viewer, operator and manager",
],
},
{
- title: "Built for Growth",
+ title: "Open and Extensible",
items: [
- "REST API and webhooks for automation and integrations",
- "Dashboard designed for both homelabs and enterprise fleets",
- "Continuous improvements with focus on reliability and usability",
+ "REST API with API keys and an OpenAPI reference",
+ "Agents for the ESP32 and for Linux, in as many networks as you like",
+ "Open source under the AGPL-3.0, hosted or self-hosted",
],
},
];
@@ -39,8 +39,8 @@ const highlightCards = [
Platform Features
- Everything you need to discover devices, trigger remote power actions, and keep your
- infrastructure available from anywhere.
+ Everything you need to power devices on and off remotely, see whether they're online,
+ and share them with the people who need them.
- Take control of your network infrastructure with NetIgnite. Remotely manage, monitor,
- and power on devices across your entire network with enterprise-grade security and
- reliability.
+ Power on, monitor and shut down the computers in your network from anywhere. A small
+ agent in your LAN does the work; you use the dashboard, your phone or the API.
- Free for personal use with up to 5 devices.
+ Open source under the AGPL-3.0. Use the hosted app or run your own server.
@@ -97,7 +96,8 @@ const heroDevices = [
Powerful Remote Management
- Everything you need to manage your network devices remotely with confidence and security.
+ Everything you need to power your devices on and off remotely and see whether they're
+ online.
@@ -142,7 +142,7 @@ const heroDevices = [
Ready to Power Up Your Network?
- Join thousands of IT professionals who trust NetIgnite for remote device management.
+ Set up your first agent in minutes, on the hosted app or your own server.
/ws/agent` and announces itself.
-#define C_CONTROL_SERVER_HOST "example.com"
-#define C_CONTROL_SERVER_PORT (uint16_t) 80
-#define C_CONTROL_SERVER_USE_SSL (bool) false
+A start looks like this on the serial console (115200 baud):
-#define C_CONTROL_SERVER_AUTH_ID "AUTH_ID"
-#define C_CONTROL_SERVER_AUTH_SECRET "AUTH_SECRET"
+```text [Serial console]
+[ 0] I NetIgnite Agent 2.0.0 on ESP32-D0WD-V3 rev 3.1
+[ 0] I Device Office-ESP32 (24:6F:28:AA:BB:CC), partition app0, reset reason: power-on
+[ 0] I Connecting to Wi-Fi "MyWiFi"
+[ 3] I Wi-Fi connected: MyWiFi, IPv4 192.168.1.50, RSSI -58 dBm
+[ 3] I Time sync started
+[ 4] I Enrolling with https://netignite.example.com
+[ 5] I Enrolled as agent 3
+[ 5] I Connecting to https://netignite.example.com/ws/agent
+[ 6] I Connected to the server as agent 3
```
-`src/config/config.h` is ignored by Git, so your credentials stay out of the repository.
+The number is the time since start in seconds, the letter the level (`E`rror, `W`arning, `I`nfo,
+`D`ebug).
-### WiFi
-
-| Setting | Description |
+| Event | Behavior |
| --- | --- |
-| `C_WIFI_SSID` | Name of the 2.4 GHz network. |
-| `C_WIFI_PASSWORD` | WiFi password. |
-| `C_WIFI_LOCAL_IP` | Static IPv4 address of the ESP32. Pick a free address outside your DHCP range. |
-| `C_WIFI_GATEWAY_IP` | Your router's address. |
-| `C_WIFI_SUBNET_MASK` | Subnet mask. The agent also uses it to compute the Wake-on-LAN broadcast address (for example `192.168.1.255`). |
-| `C_WIFI_PRIMARY_DNS`, `C_WIFI_SECONDARY_DNS` | DNS servers, used to resolve the server host and the NTP servers. |
+| Wi-Fi lost | The ESP32 reconnects on its own; after 30 seconds without a connection it starts over. |
+| Server unreachable or the credential rejected | Retries after 5 seconds, then doubles the wait up to 60 seconds. |
+| No message from the server for 100 seconds | Drops the connection and reconnects (the server pings every 30 seconds). |
+| Token rejected (used, expired or for a Linux agent) | Logs `Enrollment rejected: …` and forgets the token. Create a new one. |
+| Server not reachable while registering | Logs `Enrollment failed: …` and retries every 30 seconds. |
-::tip
-The agent always applies the static network settings. To use DHCP instead, set
-`C_WIFI_LOCAL_IP` to `IPAddress(0, 0, 0, 0)`. The ESP32 Arduino core then requests an address
-from your router; reserve one for the ESP32 there so it doesn't change.
-::
+## Serial console
-### Server
+The firmware has a small command console on its USB serial port at 115200 baud. Open it with
+**Logs & console** in the web installer dialog, PlatformIO's serial monitor (`pio device monitor`)
+or any serial terminal. Type `help` for the list.
-| Setting | Description |
+| Command | What it does |
| --- | --- |
-| `C_CONTROL_SERVER_HOST` | Host name or IP of the NetIgnite server, without `https://` or a path. |
-| `C_CONTROL_SERVER_PORT` | Server port: `443` behind an HTTPS proxy, `18232` for a direct connection to the Docker container. |
-| `C_CONTROL_SERVER_USE_SSL` | `true` for an encrypted connection (`wss://`), `false` for plain `ws://`. |
-| `C_CONTROL_SERVER_AUTH_ID` | The agent's **Connection ID**, as a string, for example `"3"`. |
-| `C_CONTROL_SERVER_AUTH_SECRET` | The agent's **Connection Secret**. |
-
-The agent always connects to the path `/api/agent-control-service`.
-
-| Server | `HOST` | `PORT` | `USE_SSL` |
-| --- | --- | --- | --- |
-| Hosted app | `"app.netignite.dev"` | `443` | `true` |
-| Self-hosted behind an [HTTPS proxy](/docs/server/reverse-proxy) | `"netignite.example.com"` | `443` | `true` |
-| Self-hosted, direct to the container (LAN only) | `"192.168.1.10"` | `18232` | `false` |
-
-::warning
-Without SSL, the agent's ID and secret are sent unencrypted. Only use plain connections inside a
-network you trust.
-::
+| `help` | Lists the commands. |
+| `status` | Firmware, device, Wi-Fi, IP addresses, server, agent ID and connection state. Never shows the secret. |
+| `setup ` | Connects the agent to a NetIgnite server with a setup token from the dashboard. |
+| `wifi [password]` | Connects to a Wi-Fi network. It's saved only if the connection works. Quote names with spaces: `wifi "My Network" "my password"`. |
+| `server ` | Changes the server address. |
+| `enroll ` | Sets the agent up again with a new token. |
+| `ip dhcp` | Gets the IP address from DHCP (the default). |
+| `ip static [dns1] [dns2]` | A static IPv4 configuration. |
+| `name ` | Host name: letters, digits and `-`, at most 63 characters. Used after a restart. |
+| `log ` | Log level. |
+| `reboot` | Restarts the agent. |
+| `factory-reset confirm` | Erases Wi-Fi, server, credential and all other settings, then restarts. |
+
+To set up a board over the serial console instead of with the installer's setup data (for example
+after flashing a firmware you built yourself):
+
+1. Connect it to Wi-Fi: `wifi "My Network" "my password"`.
+2. On the agent's page in the dashboard, click **Create setup command** and paste the command:
+ `setup https://netignite.example.com netignite_enroll_…`.
+
+A server address without `http://` or `https://` means `https://`. The default name is
+`NetIgnite-` plus the last six digits of the board's MAC address; the web installer sets the
+agent's name from the dashboard instead.
+
+## What the agent does
+
+The agent implements these commands of the [agent protocol](/docs/agents/protocol):
-## Certificate bundle
+| Command | What the agent does |
+| --- | --- |
+| `WAKE` | Sends the magic packet three times, 100 ms apart, to its subnet's broadcast address and to `255.255.255.255`. |
+| `STATUS` | Pings the device twice with a 1-second timeout (IPv4, IPv6, or the IPv6 link-local address derived from the MAC address) and tries the TCP port, if one is set. A refused TCP connection also counts as online. Host names resolve to IPv4 and IPv6 addresses. |
+| `SSH_CHECK`, `SSH_EXEC` | Logs in to a device over SSH (one login at a time). See [SSH](/docs/guides/ssh). |
+| `OTA_BEGIN` | Installs a firmware update; see [below](#updates-over-the-network). |
+| `GET_LOGS`, `LOG_SUBSCRIBE` | Sends its stored log (the last 16 KB, including ESP-IDF's own messages) and new lines for the dashboard's console. |
+| `GET_INFO`, `CONFIG_SET` | Reports diagnostics; changes the server address, host name, log level and IP settings. |
+| `REBOOT`, `FACTORY_RESET` | Restarts; erases its settings and restarts. |
-The firmware embeds a bundle of trusted root certificates (`data/cert/x509_crt_bundle.bin`) that
-it uses to verify the server's TLS certificate. PlatformIO generates the bundle automatically
-before each build from `data/cert/cacrt_all.pem`, Mozilla's list of public root CAs (including
-Let's Encrypt). On the first build it installs the Python package `cryptography` into
-PlatformIO's environment, which needs internet access.
+The dashboard's [Console](/docs/guides/agents#console) tab uses the log and settings commands.
-If your server uses a certificate from a private CA, put that CA's certificate (`.pem` or `.der`)
-into `data/cert/custom/` and rebuild. It's added to the bundle; remove the file to take it out
-again.
+## Updates over the network
-## Build and flash
+The firmware has two app slots of 1.875 MB each. An update from the dashboard
+([Firmware updates](/docs/guides/agents#firmware-updates)) works like this:
-Connect the board over USB, then build, upload and open the serial monitor:
+1. The server sends `OTA_BEGIN` with the version, size, SHA-256 and, for signed releases, the
+ signature of the new firmware.
+2. The agent downloads it from its own server (`/api/v1/firmware//files/firmware.bin`)
+ with its own credential, writes it into the unused slot and checks size and SHA-256. A firmware
+ that requires signed updates also checks the signature and refuses unsigned ones.
+3. It restarts into the new firmware, which is **on probation**: it marks itself as good once it
+ has reached the server. If it crashes or doesn't get there within **5 minutes**, the board goes
+ back to the previous firmware and reports the failed version to the server.
-```bash [Terminal]
-pio run
-pio run -t upload
-pio device monitor
-```
+While the new firmware is on probation, the agent refuses another update. If memory is short, it
+pauses the server connection during the download.
-In VS Code, use the PlatformIO toolbar's **Build**, **Upload** and **Serial Monitor** buttons
-instead. The monitor runs at 115200 baud. Add `--upload-port ` (for example `COM5` or
-`/dev/ttyUSB0`) if PlatformIO doesn't find the board.
+### Release signing
-::note
-The project targets the Arduino-ESP32 **2.x** core that PlatformIO's `espressif32` platform
-provides. Arduino core 3.x (for example via pioarduino) isn't supported yet.
-::
+Releases can be signed with an ECDSA P-256 key. Signing works at two points:
-Every configuration change requires a rebuild and upload. The firmware has no setup portal and no
-over-the-air updates.
-
-## What happens at boot
-
-A successful start looks like this on the serial monitor (shortened; the order of the lines can
-vary):
-
-```text [Serial Monitor]
-[INFO] Starting...
-[INFO] Connecting to Wifi...
-[INFO] STA Configured
-[INFO] Connecting to MyWiFi...
-[INFO] Wifi connected to MyWiFi. Waiting for IP...
-[INFO] IPv6 enabled on WiFi
-[INFO] Got IP address!
-[INFO] IP address: 192.168.1.32
-[INFO] Waiting for NTP time sync: ....
-[INFO] Current UTC time: Sun Oct 18 19:30:00 2026
-[INFO] Websocket Connected to netignite.example.com:443 SSL: true
-```
+- **On the agent.** A firmware built with a public key (`keys/firmware-signing.pub.pem` in the
+ repository at build time) only installs updates with a valid signature. `status` on the serial
+ console shows `updates: signed only` then; the dashboard's firmware card says the agent checks
+ the *signature and SHA-256*.
+- **On the server.** With
+ [`NETIGNITE_FIRMWARE_PUBLIC_KEY`](/docs/server/configuration#esp32-firmware) set to the same
+ public key, the server only syncs releases with a valid `firmware.sig`. The admin's firmware list
+ marks them as *signed*.
-1. The agent connects to WiFi and enables IPv6.
-2. It waits up to 30 seconds for the clock to sync via NTP, which is needed to check TLS
- certificates.
-3. It opens the WebSocket to the server and authenticates with the Connection ID and Secret.
-4. It answers commands until the connection drops.
+To sign releases of your own fork, create a key pair with `sh scripts/generate_signing_key.sh`,
+store the private key as the repository secret `FIRMWARE_SIGNING_KEY` and commit only the public
+key. The release workflow then attaches `firmware.sig` to every release.
-| Event | Behavior |
-| --- | --- |
-| WiFi connection lost | Reconnects after 60 seconds. |
-| Server connection lost or refused | Retries every 60 seconds. |
-| No WiFi or NTP during boot | After 30 seconds the agent logs `NTP time sync timed out` and continues. It keeps retrying WiFi and the server every 60 seconds; TLS connections succeed once the time is synced. |
-| Empty Connection ID or Secret | Logs `Missing credentials!` and never connects. |
+## Certificates
-## Commands
+For `https://` servers, the agent checks the server's certificate against a bundle of trusted root
+certificates compiled into the firmware: Mozilla's list of public root CAs, which includes Let's
+Encrypt. A certificate from a private CA only works with a firmware you build yourself with that CA
+added (see below). Don't update such an agent to an official release over the network: the official
+firmware doesn't trust your CA, can't reach the server and goes back to your build after 5 minutes.
-The agent implements the three commands of the [agent protocol](/docs/agents/protocol):
+## Build from source
-| Command | What the agent does |
-| --- | --- |
-| `WAKEUP` | Validates the MAC address and port, then sends the Wake-on-LAN magic packet three times, 100 ms apart, to the subnet's broadcast address. |
-| `GET_STATUS` | Pings the device once with a 1-second timeout. A MAC address is converted to the device's IPv6 link-local (EUI-64) address; an IPv4 address is pinged directly. |
-| `HEARTBEAT` | Nothing; the server uses it to keep the connection active. |
+You need [PlatformIO](https://platformio.org/) Core **6.2 or newer** (`pio upgrade`). The build uses
+[pioarduino](https://github.com/pioarduino/platform-espressif32) with Arduino-ESP32 3.3 on ESP-IDF
+5.5, pinned in `platformio.ini`.
-MAC addresses must have the form `AA:BB:CC:DD:EE:FF` or `AA-BB-CC-DD-EE-FF`. SecureOn (password
-protected) magic packets aren't supported.
+```bash [Terminal]
+git clone https://github.com/NetIgnite-project/Agent-ESP32.git
+cd Agent-ESP32
+pio run -e esp32dev # build
+pio run -e esp32dev -t upload # flash over USB
+pio device monitor # serial console, 115200 baud
+```
+
+Add `--upload-port ` (for example `COM5` or `/dev/ttyUSB0`) if PlatformIO doesn't find the
+board. A board flashed this way has no setup data: set it up over the
+[serial console](#serial-console).
+
+- **Private CA:** put its certificate (`.pem` or `.der`) into `data/cert/custom/` before building.
+ The build adds it to the certificate bundle. The bundle generator needs the Python package
+ `cryptography`, which the build installs on the first run.
+- **Signed updates:** with `keys/firmware-signing.pub.pem` present, the firmware only accepts
+ signed updates (see [Release signing](#release-signing)).
+- **Version:** the build takes it from `git describe`, or from the environment variable
+ `NETIGNITE_FW_VERSION`.
+
+::tip
+On Windows, build from PowerShell or the Command Prompt rather than Git Bash: ESP-IDF's tools
+refuse to run in an MSYS shell.
+::
## Limitations
-- The agent uses IPv4 subnet broadcasts for Wake-on-LAN, so it only reaches devices in its own
- subnet. Use one agent per network.
-- The status check depends on the device's IPv6 link-local address being derived from its MAC
- address. See [How It Works](/docs/how-it-works#checking-device-status).
-- The `installer/` folder in the repository is an early placeholder for a desktop installer. It
- can't flash boards yet; use PlatformIO.
+- Wake-on-LAN uses IPv4 broadcasts, so the agent only reaches devices in its own subnet. Use one
+ agent per network.
+- The *MAC (IPv6 link-local)* status check only works for devices that use their MAC-based IPv6
+ address. See [Status checks](/docs/guides/devices#status-checks).
+- Wi-Fi can only be changed over USB, never through the server.
+- One SSH login runs at a time; a second one at the same moment is refused.
diff --git a/content/docs/agents/linux.md b/content/docs/agents/linux.md
new file mode 100644
index 0000000..eb7f4f4
--- /dev/null
+++ b/content/docs/agents/linux.md
@@ -0,0 +1,343 @@
+---
+title: "Linux Agent"
+description: "The NetIgnite agent for Linux: install it with one command, pair it, host mode and Wake-on-LAN mode, the systemd service, polkit, uninstalling and security."
+navigation:
+ title: Linux Agent
+---
+
+# Linux Agent
+
+`netignite-agent` connects a Linux machine to your NetIgnite server. It has two modes:
+
+- **Host mode** (always on): the dashboard sees whether this machine is online and can **shut it
+ down or reboot it**.
+- **Wake-on-LAN mode** (optional): the agent also **wakes other devices** in its network,
+ **checks whether they're online** and **logs in to them over [SSH](/docs/guides/ssh)** to shut
+ them down or reboot them. That's what the [ESP32 agent](/docs/agents/esp32) does, on a machine
+ you already run: a Raspberry Pi, a NAS or a home server.
+
+The agent is a single binary that runs as a systemd service under its own unprivileged user. It
+talks to the server over one outgoing WebSocket; nothing listens on the machine. Source and
+releases: [NetIgnite-project/Agent-Linux](https://github.com/NetIgnite-project/Agent-Linux).
+
+## Requirements
+
+- Linux with systemd, on a 64-bit x86 or ARM CPU.
+- polkit, for shutdown and reboot (`apt install polkitd` on Debian and Ubuntu, `dnf install polkit`
+ on Fedora). Without it, the agent runs, but shutdown and reboot fail.
+- Wake-on-LAN mode: `ping` (iputils or BusyBox) and `ip` (iproute2).
+- `curl` and `sha256sum` for the install script.
+
+| Binary | For |
+| --- | --- |
+| `netignite-agent-linux-x64` | x86-64 CPUs with AVX2 (about 2013 and newer) |
+| `netignite-agent-linux-x64-baseline` | Any x86-64 CPU |
+| `netignite-agent-linux-arm64` | 64-bit ARM, for example a Raspberry Pi 3, 4 or 5 with a 64-bit OS |
+
+## Install
+
+1. In the dashboard, create an agent of the kind **Linux agent** (**Agents → Add agent**).
+2. On its page, click **Create setup command**.
+3. Run the command on the Linux machine within 15 minutes:
+
+```bash [Terminal]
+curl -fsSL https://netignite.example.com/install/linux.sh | sudo sh -s -- netignite_enroll_…
+```
+
+The script, served by your NetIgnite server:
+
+1. checks that it runs on Linux as root with `curl`, `sha256sum` and systemd,
+2. picks the binary for the CPU: `arm64`, `x64` if the CPU has AVX2, `x64-baseline` otherwise,
+3. downloads it and `SHA256SUMS` from the
+ [GitHub releases](https://github.com/NetIgnite-project/Agent-Linux/releases) and checks the
+ checksum,
+4. [pairs](#pair) the agent with the server using the token,
+5. [installs](#the-systemd-service) the systemd service and starts it.
+
+The agent shows up as online on its page. Then:
+
+- To control **this machine**, open (or add) the device for it, choose the agent under
+ **Linux agent on the device**, and choose the status check **Linux agent on the device**. The
+ device gets **Shut down** and **Reboot**.
+- To **wake other devices**, turn on [Wake-on-LAN mode](#wake-on-lan-mode) and choose the agent as
+ the **Agent** of those devices.
+
+By default, the script installs the newest stable release (a pre-release only while there is no
+stable one). The server's admin can pin a release with `NETIGNITE_LINUX_AGENT_VERSION` or use
+another repository with `NETIGNITE_LINUX_AGENT_REPO` (see
+[Configuration](/docs/server/configuration#linux-agent-installer)).
+On the machine, the variables `NETIGNITE_AGENT_VERSION` (a release tag) and
+`NETIGNITE_AGENT_BASE_URL` (a mirror with the release files) override that; pass them through
+`sudo env`:
+
+```bash [Terminal]
+curl -fsSL https://netignite.example.com/install/linux.sh | sudo env NETIGNITE_AGENT_VERSION=v1.0.0 sh -s -- netignite_enroll_…
+```
+
+### Manual install
+
+Without the script, or if the machine reaches the server under another address than the
+dashboard's:
+
+```bash [Terminal]
+arch=x64 # or x64-baseline, arm64
+base=https://github.com/NetIgnite-project/Agent-Linux/releases/latest/download
+curl -fsSLO "$base/netignite-agent-linux-$arch"
+curl -fsSLO "$base/SHA256SUMS"
+sha256sum --check --ignore-missing SHA256SUMS
+chmod +x "netignite-agent-linux-$arch"
+
+sudo ./netignite-agent-linux-$arch pair https://netignite.example.com netignite_enroll_…
+sudo ./netignite-agent-linux-$arch install
+```
+
+`releases/latest` skips pre-releases. `install` copies the binary to
+`/usr/local/bin/netignite-agent`.
+
+### Updating
+
+Run the install script again without a token. It downloads the current release and reinstalls the
+service with the existing pairing:
+
+```bash [Terminal]
+curl -fsSL https://netignite.example.com/install/linux.sh | sudo sh
+```
+
+## Pair
+
+```bash [Terminal]
+sudo netignite-agent pair [--wol] [--config ]
+```
+
+- The agent generates its own secret (32 random bytes), sends it with the token to
+ `POST /api/v1/agents/enroll` and writes the result to `/etc/netignite/agent.json`
+ (mode `0600`). Neither the secret nor the token is printed.
+- `` is the server's address, for example `https://netignite.example.com` or
+ `http://192.168.1.10:18232`. A missing scheme means `https://`.
+- Pairing again with a new token replaces the credential. The server disconnects the old one, and
+ an installed service is restarted with the new one.
+- A token of an ESP32 agent is refused and stays valid for that ESP32. Create a Linux agent for this
+ machine instead.
+- `--wol` turns on Wake-on-LAN mode. Without it, pairing again keeps the previous mode.
+
+The config file looks like this:
+
+```json [agent.json]
+{
+ "server": "https://netignite.example.com",
+ "agent_id": 3,
+ "token_id": "…",
+ "secret": "…",
+ "wol": { "enabled": false }
+}
+```
+
+## Host mode
+
+In host mode, the agent announces the capabilities `host.status` and `host.control`. The server can
+then:
+
+- show the machine as online while the agent is connected (status check
+ *Linux agent on the device*),
+- send `HOST_CONTROL` with `shutdown` or `reboot`. The agent first asks systemd-logind whether it
+ may (`CanPowerOff`/`CanReboot`); if not (polkit rule missing, or a block inhibitor such as a
+ running package upgrade), it answers with the reason, which the dashboard shows. Otherwise it
+ answers, waits about a second and runs `systemctl --no-ask-password poweroff` or
+ `systemctl --no-ask-password reboot`.
+
+Nothing else: the server can't run commands, read files or change the configuration. If
+`systemctl` still fails after the answer, the error is in the agent's journal.
+
+## Wake-on-LAN mode
+
+```bash [Terminal]
+sudo netignite-agent wol-mode on # or: pair --wol
+sudo netignite-agent wol-mode off
+```
+
+`wol-mode` updates the config and the installed service, and restarts it. In this mode, the agent
+also announces `wake`, `status.link_local`, `status.ip`, `status.host`, `status.tcp` and `ssh`:
+
+- **Wake:** a magic packet over UDP to `255.255.255.255` and each network interface's broadcast
+ address, three times, 100 ms apart.
+- **Status:** the device is online if it answers a ping (`ping -c 1 -W 1`) or the TCP port (a
+ refused connection counts too). For the MAC check, the agent first looks at the neighbor table
+ (`ip neigh`): a reachable entry for the MAC address counts as online. Otherwise it pings the
+ IPv6 link-local address derived from the MAC address and the addresses the neighbor table knows
+ for it. Every check answers within 4.5 seconds.
+- **SSH:** logins to devices in the network for [shutdown and reboot over SSH](/docs/guides/ssh).
+
+To test Wake-on-LAN from the machine itself:
+
+```bash [Terminal]
+netignite-agent wol AA:BB:CC:DD:EE:FF --port 9
+```
+
+## The systemd service
+
+`install` needs root and a paired config. It:
+
+- creates the system user `netignite` (no home directory, no login shell),
+- copies the binary to `/usr/local/bin/netignite-agent`,
+- makes the config readable for the service: `/etc/netignite` owned by `root:netignite` with mode
+ `0750`, `agent.json` owned by `netignite` with mode `0600`,
+- writes `/etc/systemd/system/netignite-agent.service`,
+- writes the polkit rule `/etc/polkit-1/rules.d/50-netignite-agent.rules` (plus a `.pkla` file in
+ `/etc/polkit-1/localauthority/50-local.d/` on polkit older than 0.106, for example Debian 11),
+- runs `systemctl daemon-reload`, `enable` and `restart`.
+
+`sudo netignite-agent install --dry-run` prints every step and the files' contents without changing
+anything. The unit for host mode (comments left out):
+
+```ini [netignite-agent.service]
+[Unit]
+Description=NetIgnite Agent
+Documentation=https://github.com/NetIgnite-project/Agent-Linux
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=simple
+User=netignite
+Group=netignite
+ExecStart=/usr/local/bin/netignite-agent run --config /etc/netignite/agent.json
+Restart=always
+RestartSec=5
+RestartPreventExitStatus=78
+TimeoutStopSec=10
+
+NoNewPrivileges=yes
+ProtectSystem=strict
+ProtectHome=yes
+PrivateTmp=yes
+PrivateDevices=yes
+ReadOnlyPaths=/etc/netignite
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectKernelLogs=yes
+ProtectControlGroups=yes
+ProtectClock=yes
+ProtectHostname=yes
+RestrictNamespaces=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+LockPersonality=yes
+SystemCallArchitectures=native
+UMask=0077
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
+CapabilityBoundingSet=
+
+[Install]
+WantedBy=multi-user.target
+```
+
+In Wake-on-LAN mode, the last line is replaced by `CapabilityBoundingSet=CAP_NET_RAW` and
+`AmbientCapabilities=CAP_NET_RAW`, which `ping` needs. Use `systemctl edit netignite-agent` for local
+changes; `install` and `wol-mode` overwrite the unit file.
+
+### polkit
+
+The rule lets the user `netignite`, and nobody else, use exactly the logind actions behind
+`systemctl poweroff` and `systemctl reboot`:
+
+```js [polkit rule]
+polkit.addRule(function (action, subject) {
+ var allowed = [
+ "org.freedesktop.login1.power-off",
+ "org.freedesktop.login1.power-off-multiple-sessions",
+ "org.freedesktop.login1.reboot",
+ "org.freedesktop.login1.reboot-multiple-sessions"
+ ];
+ if (subject.user == "netignite" && allowed.indexOf(action.id) >= 0) {
+ return polkit.Result.YES;
+ }
+});
+```
+
+The `*-multiple-sessions` actions apply when other users are logged in, for example over SSH. The
+`*-ignore-inhibit` actions aren't granted, so a block inhibitor (a running package upgrade, for
+example) still stops a remote shutdown. `systemctl` asks logind over D-Bus, and logind asks polkit;
+no setuid helper is involved, so `NoNewPrivileges` doesn't get in the way.
+
+## Commands
+
+| Command | What it does |
+| --- | --- |
+| `pair ` | Registers with the server and writes the config (`--wol`, `--config`). |
+| `install` | Sets up and starts the systemd service (`--config`, `--dry-run`). |
+| `uninstall` | Removes the service (`--purge`, `--config`, `--dry-run`). |
+| `run` | Runs the agent in the foreground; this is what the service runs (`--wol`, `--config`). |
+| `status` | Shows the config, the server, the agent ID, the service state and whether the server and the credential work. Never shows the secret. |
+| `wol-mode on\|off` | Turns Wake-on-LAN mode on or off (`--config`). |
+| `wol ` | Sends a magic packet from this machine (`--port`, default `9`). |
+| `version` | Prints the version. |
+
+Global flags go before the command, for example `netignite-agent --log-level debug run`. The
+default config path is `/etc/netignite/agent.json`.
+
+Exit codes: `0` success, `1` error, `78` the server rejected the credential. The service isn't
+restarted after `78`; pair again.
+
+```bash [Terminal]
+sudo netignite-agent status
+journalctl -u netignite-agent -f
+```
+
+## Connection
+
+The agent keeps one WebSocket to `/ws/agent` (`wss://` for `https://` servers) and speaks
+the [agent protocol](/docs/agents/protocol). Its `HELLO` reports the version, the capabilities and,
+for the dashboard, the host name, OS, kernel, architecture, uptime, IP addresses and whether
+Wake-on-LAN mode is on.
+
+- It reconnects after 5 seconds, doubling the wait up to 60 seconds. If the server sends nothing for
+ 100 seconds (it pings every 30 seconds), the agent drops the connection and reconnects.
+- It stops for good and exits with `78` when the server rejects the credential (HTTP 401), revokes
+ it (close code `4003`, for example after pairing again or **Revoke credential**) or deletes the
+ agent (`4004`).
+- If another connection with the same credential takes over (`4000`), it waits 60 seconds before
+ reconnecting, so two copies of the agent don't keep replacing each other.
+
+## Uninstall
+
+```bash [Terminal]
+sudo netignite-agent uninstall # keeps /etc/netignite/agent.json (root-only again)
+sudo netignite-agent uninstall --purge # also deletes the config with the credential
+```
+
+This stops and disables the service and removes the unit, the polkit rule, the user `netignite`
+and `/usr/local/bin/netignite-agent`. `--dry-run` shows the steps. Delete the agent in the dashboard
+too.
+
+## Security
+
+- **Fixed actions only.** The server can ask for `PING`, `HOST_CONTROL` (`shutdown` or `reboot`)
+ and, in Wake-on-LAN mode, `WAKE`, `STATUS`, `SSH_CHECK` and `SSH_EXEC` (one of four fixed commands
+ on another device). Every parameter is validated; commands run without a shell, and only IP
+ addresses reach `ping`. There's no remote shell, no file access and no configuration change from
+ the server.
+- **The secret** is generated on the machine and only leaves it in the `Authorization` header. The
+ server stores only a hash. It's in `/etc/netignite/agent.json` with mode `0600` and never printed,
+ not even by `status`. Use an `https://` server address outside your LAN.
+- **The service** runs as the unprivileged user `netignite` with the hardening options above, without
+ capabilities in host mode and with only `CAP_NET_RAW` in Wake-on-LAN mode.
+- **SSH logins** for other devices arrive with each request and are only kept in memory for that
+ login; the agent never writes or logs them. See [SSH](/docs/guides/ssh#security).
+- The binary doesn't load `.env` or `bunfig.toml` from the working directory, so a stray file there
+ can't change how `pair` or `install` behave when run as root.
+
+## Building from source
+
+The agent is written in TypeScript and compiled with [Bun](https://bun.sh) into single binaries.
+
+```bash [Terminal]
+git clone https://github.com/NetIgnite-project/Agent-Linux.git
+cd Agent-Linux
+bun install
+bun test
+bun run compile all --no-version-tag # build/bin/netignite-agent-linux-{x64,x64-baseline,arm64}
+```
+
+Releases are built by the repository's release workflow when a tag `v` is pushed; it
+attaches the three binaries and `SHA256SUMS`. Versions with a suffix, such as `1.0.0-beta.1`, are
+published as pre-releases.
diff --git a/content/docs/agents/protocol.md b/content/docs/agents/protocol.md
index 38b8148..a7aa00c 100644
--- a/content/docs/agents/protocol.md
+++ b/content/docs/agents/protocol.md
@@ -1,197 +1,453 @@
---
-title: "Agent Protocol"
-description: "The WebSocket protocol between the NetIgnite server and its agents: connection, authentication, message format and commands."
+title: "Agent Protocol v2"
+description: "The WebSocket protocol between the NetIgnite server and its agents: enrollment, frames, HELLO, capabilities, every command and event, close codes and an example agent."
navigation:
title: Agent Protocol
---
-# Agent Protocol
+# Agent Protocol v2
-Agents talk to the NetIgnite server over a single WebSocket connection that the agent opens. The
-protocol is plain text and small enough to implement in a few dozen lines, so you can write an
-agent for any platform: a Raspberry Pi, a NAS or a router.
+Agents talk to the NetIgnite server over one WebSocket that the agent opens. Messages are JSON, and
+the server only sends an agent the commands it said it supports, so you can write a small agent for
+any platform. The server's source of truth is
+[`server/lib/agents/protocol.ts`](https://github.com/NetIgnite-project/Server/blob/main/server/lib/agents/protocol.ts).
+
+::note
+Protocol v2 replaces the v1 protocol (`/api/agent-control-service`, `COMMAND:ID:JSON` text frames,
+HTTP Basic authentication). v1 agents can't connect to a v2 server.
+::
+
+## Overview
+
+1. The agent **enrolls** once with a single-use token and a secret it generated, and gets a token
+ ID.
+2. It **connects** to `/ws/agent` with its credential and sends `HELLO` with its capabilities.
+3. The server sends **requests**; the agent answers each one. The agent can send **events** at any
+ time after `HELLO`.
+
+## Enrollment
+
+The owner creates an enrollment token in the dashboard (**Create setup command** on the agent's
+page, or `POST /api/v1/agents//enrollments`). It looks like `netignite_enroll_:`,
+works once and expires after 15 minutes.
+
+The agent generates a secret of 32 to 128 characters from `A-Z a-z 0-9 _ -` (the official agents
+use 32 random bytes as 64 hex characters) and sends it with the token:
+
+```http
+POST /api/v1/agents/enroll HTTP/1.1
+Host: netignite.example.com
+Content-Type: application/json
+
+{ "enrollment_token": "netignite_enroll_…", "secret": "", "kind": "linux" }
+```
+
+`kind` (`esp32` or `linux`) is optional. With it, the server refuses a token that belongs to an
+agent of another kind before using it up.
+
+```json
+{
+ "success": true,
+ "code": 200,
+ "message": "Agent enrolled",
+ "data": { "agent_id": 3, "kind": "linux", "token_id": "9f2c…" }
+}
+```
+
+| Status | Meaning |
+| --- | --- |
+| `200` | Enrolled. The agent's credential is `netignite_agent_:`. |
+| `400` | Invalid body or secret. |
+| `401` | Unknown, used or expired token. Get a new one. |
+| `409` | The token is for another kind of agent. It stays valid. |
+| `429` | More than 20 attempts from one address in 5 minutes. |
+
+The server stores only a hash of the secret. Enrolling again replaces the agent's credential and
+disconnects a connection that uses the old one (close code `4003`).
## Connecting
| Setting | Value |
| --- | --- |
-| **URL** | `wss:///api/agent-control-service` (or `ws://` without TLS) |
-| **Port** | The server's HTTP port: `443` behind a proxy, `18232` for the Docker container |
-| **Authentication** | HTTP Basic, sent with the WebSocket upgrade request |
+| **URL** | `wss:///ws/agent` (`ws://` for an `http://` server) |
+| **Authentication** | `Authorization: Bearer netignite_agent_:` on the upgrade request |
| **Subprotocol** | None |
+| **Frames** | UTF-8 text frames with one JSON object each, at most 64 KiB from the agent |
```http
-GET /api/agent-control-service HTTP/1.1
+GET /ws/agent HTTP/1.1
Host: netignite.example.com
Upgrade: websocket
Connection: Upgrade
-Authorization: Basic MzpzM2NyM3Q=
+Authorization: Bearer netignite_agent_9f2c…:3a7d…
```
-The credentials are `base64(":")`; the example above encodes `3:s3cr3t`. The ID
-is the agent's numeric **Connection ID** and the secret its **Connection Secret** (see
-[Managing Agents](/docs/guides/agents)).
-
-- The server accepts the upgrade first and checks the credentials afterwards. With a missing or
- wrong `Authorization` header, it closes the connection right away with code `1008` and reason
- `Missing or invalid credentials`.
-- Each agent can have one connection. When an agent connects again, the server closes the older
- connection.
-- If the agent's secret is regenerated or the agent is deleted, the server closes the connection.
+- Without a valid credential, the server answers the upgrade with HTTP `401`.
+- Each agent has one connection. When it connects again, the server closes the older connection
+ with `4000`.
- Browsers can't set the `Authorization` header on WebSockets, so agents can't run in a browser.
-## Message format
+## Frames
-Every message is a UTF-8 **text frame** with three parts separated by colons:
+```json
+{ "t": "req", "id": 7, "cmd": "WAKE", "p": { "mac": "AA:BB:CC:DD:EE:FF", "port": 9 } }
+{ "t": "res", "id": 7, "ok": true, "p": {} }
+{ "t": "res", "id": 7, "ok": false, "err": "The agent has no network connection" }
+{ "t": "evt", "cmd": "HELLO", "p": { … } }
+```
-```text
-::
+| Field | Description |
+| --- | --- |
+| `t` | `req`: a request from the server. `res`: the agent's answer. `evt`: an event from the agent, without an answer. |
+| `id` | `req`/`res`: a number from `0` to `4294967295`. The answer carries the request's `id`. |
+| `cmd` | `req`/`evt`: the command or event name, for example `WAKE`. Case-sensitive. |
+| `p` | The payload object. Optional in `res`; defaults to `{}`. |
+| `ok`, `err` | `res`: whether the request succeeded, and an error message of up to 1024 characters. The dashboard shows `err` to the user. |
+
+Several requests can be in flight; answer them in any order. The server drops frames that aren't
+valid JSON or don't match this shape, answers requests from the agent with
+`{ "ok": false, "err": "Unknown command …" }`, and ignores answers to unknown IDs.
+
+## HELLO
+
+The first frame the agent sends. It must arrive within 10 seconds of connecting.
+
+```json
+{
+ "t": "evt",
+ "cmd": "HELLO",
+ "p": {
+ "protocol": 2,
+ "kind": "linux",
+ "version": "1.0.0",
+ "capabilities": ["host.status", "host.control"],
+ "info": { "hostname": "nas", "uptime_s": 86400 }
+ }
+}
```
-| Part | Description |
+| Field | Description |
+| --- | --- |
+| `protocol` | `2`. |
+| `kind` | `esp32` or `linux`; must match the agent's kind in the dashboard. |
+| `version` | The agent's version, 1–64 characters. Shown in the dashboard and compared for firmware updates. |
+| `capabilities` | What the agent supports, up to 64 strings. Unknown ones are kept, so newer agents work with older servers. |
+| `info` | Optional diagnostics, shown as key/value pairs on the agent's page. |
+
+An invalid `HELLO` closes the connection with `4001`. Events other than `HELLO` before it are
+ignored. A later `HELLO` updates the stored capabilities, version and info.
+
+## Capabilities
+
+| Capability | Enables | ESP32 | Linux, host mode | Linux, Wake-on-LAN mode |
+| --- | --- | --- | --- | --- |
+| `wake` | `WAKE` | ✓ | | ✓ |
+| `status.link_local` | `STATUS` with a `link_local` target | ✓ | | ✓ |
+| `status.ip` | `STATUS` with an `ip` target | ✓ | | ✓ |
+| `status.host` | `STATUS` with a `host` target | ✓ | | ✓ |
+| `status.tcp` | `STATUS` with a `tcp_port` | ✓ | | ✓ |
+| `host.status` | The agent runs on the device it reports (`HOST_STATUS` events) | | ✓ | ✓ |
+| `host.control` | `HOST_CONTROL` | | ✓ | ✓ |
+| `ota` | `OTA_BEGIN` and `OTA_PROGRESS` (ESP32 agents only) | ✓ | | |
+| `logs` | `GET_LOGS`, `LOG_SUBSCRIBE` and `LOG` | ✓ | | |
+| `config` | `GET_INFO`, `CONFIG_SET` | ✓ | | |
+| `device.reboot` | `REBOOT` | ✓ | | |
+| `device.reset` | `FACTORY_RESET` | ✓ | | |
+| `ssh` | `SSH_CHECK`, `SSH_EXEC` | ✓ | | ✓ |
+
+`PING` needs no capability. A command without its capability fails on the server with
+*… doesn't support …* and is never sent.
+
+## Requests
+
+The server waits for each answer; after the timeout the request fails (*The agent didn't answer in
+time*).
+
+| Request | Timeout |
| --- | --- |
-| `COMMAND` | The command name, for example `WAKEUP`. Case-sensitive. |
-| `ID` | A decimal number from `0` to `4294967295` that identifies the request. |
-| `JSON` | A JSON object with the command's payload. May itself contain colons; split only at the first two. |
+| `WAKE`, `GET_LOGS`, `GET_INFO`, `CONFIG_SET`, `REBOOT`, `FACTORY_RESET` | 5 s |
+| `STATUS` | 6 s |
+| `PING`, `HOST_CONTROL`, `OTA_BEGIN`, `LOG_SUBSCRIBE`, `SSH_CHECK` | 10 s |
+| `SSH_EXEC` | 15 s |
-The server sends commands; the agent answers each one with the **same command name and ID** and a
-response object:
+### PING
-```text
-→ WAKEUP:2876151413:{"macAddress":"AA:BB:CC:DD:EE:FF","port":9}
-← WAKEUP:2876151413:{"status":"OK","message":"Wakeup successful"}
-```
+Keep-alive. The server sends it every 30 seconds after `HELLO`.
-The server waits **5 seconds** for each response; after that it treats the command as failed.
-Several commands can be pending at once, and responses may arrive in any order. They're matched by
-ID.
+| Request | Answer |
+| --- | --- |
+| `{}` | `{}` |
-## Commands
+### WAKE
-### WAKEUP
+Send a Wake-on-LAN magic packet: 6 bytes `0xFF`, then the 6-byte MAC address 16 times, as a UDP
+broadcast to the local network.
-Send a Wake-on-LAN magic packet.
+| Request field | Type | Description |
+| --- | --- | --- |
+| `mac` | string | Target MAC address, `AA:BB:CC:DD:EE:FF` (uppercase, colons). |
+| `port` | number | UDP port, `1`–`65535`, usually `9`. |
+
+Answer `{}` once the packet was sent, or `ok: false` with the reason.
+
+### STATUS
+
+Check whether a device is online. Online means *any* requested probe answered.
| Request field | Type | Description |
| --- | --- | --- |
-| `macAddress` | string | Target MAC address, as stored in the device record (for example `AA:BB:CC:DD:EE:FF`). |
-| `port` | number | UDP port for the magic packet, usually `9`. |
+| `target` | object | `{ "type": "link_local", "mac": "AA:BB:CC:DD:EE:FF" }`: the IPv6 link-local (EUI-64) address derived from the MAC. `{ "type": "ip", "address": "192.168.1.20" }`: an IPv4 or IPv6 address, possibly with a zone (`fe80::1%eth0`). `{ "type": "host", "host": "nas.local" }`: a host name. |
+| `icmp` | boolean | Ping the target. `false` for the *SSH port* check. |
+| `tcp_port` | number | Optional: also try a TCP connection to this port. |
-| Response field | Type | Description |
+| Answer field | Type | Description |
| --- | --- | --- |
-| `status` | `"OK"` \| `"ERROR"` | Whether the packet was sent. |
-| `message` | string | Human-readable result. On `ERROR`, the dashboard shows it after *Wakeup command failed:*. |
+| `online` | boolean | Whether the device answered. |
-A magic packet is 6 bytes of `0xFF` followed by the 6-byte MAC address repeated 16 times, sent as
-a UDP broadcast to the local subnet.
+Answer within the 6-second timeout; the official agents give up after 4.5 seconds. `ok: false`
+(for example an unresolvable host name) shows as *Unknown* with the error as the reason.
-### GET_STATUS
+### HOST_CONTROL
-Report whether a device is reachable.
+Shut down or reboot the machine the agent runs on.
| Request field | Type | Description |
| --- | --- | --- |
-| `address` | string | The device's address. The current server always sends the device's **MAC address** here. |
+| `action` | `"shutdown"` \| `"reboot"` | What to do. |
+
+Answer `{}` first, then act: once the machine goes down, the answer can't be sent anymore.
+
+### OTA_BEGIN
-| Response field | Type | Description |
+Install a firmware update (ESP32 agents).
+
+| Request field | Type | Description |
| --- | --- | --- |
-| `status` | `"OK"` \| `"ERROR"` | Whether the check could run. |
-| `message` | string | Human-readable result. |
-| `online` | `"online"` \| `"offline"` \| `"unknown"` | The device status shown in the dashboard. |
+| `version` | string | The new version. |
+| `path` | string | Where to download the image on the agent's server, for example `/api/v1/firmware/2.1.0/files/firmware.bin`. Download it with the agent's own `Authorization` header. |
+| `size` | number | Image size in bytes. |
+| `sha256` | string | SHA-256 of the image, 64 hex characters. |
+| `signature` | string | Optional: the ECDSA P-256 signature (DER, hex) of the image. |
-How you resolve a MAC address to something you can ping is up to the agent. The ESP32 agent pings
-the device's EUI-64 IPv6 link-local address; an agent on a full computer could look the MAC up in
-its ARP/neighbor table instead.
+Answer `{}` when the download has started. Report progress with `OTA_PROGRESS` events, restart into
+the new firmware and send a `HELLO` with the new version. If the new firmware fails and the agent
+went back to the old one, its `HELLO` `info` contains `"ota_failed_version": ""`. The
+server counts an update as failed after 15 minutes.
-### HEARTBEAT
+### GET_LOGS
-Sent by the server to every connected agent once a minute, with an empty payload `{}`. The server
-doesn't wait for the answer; replying with `{}` is fine.
+| Request | Answer |
+| --- | --- |
+| `{}` | `{ "lines": ["…"], "level": "info" }`: the stored log lines, oldest first, and the log level (`error`, `warn`, `info` or `debug`). Both optional. |
-```text
-→ HEARTBEAT:1739004211:{}
-← HEARTBEAT:1739004211:{}
-```
+### LOG_SUBSCRIBE
+
+| Request field | Type | Description |
+| --- | --- | --- |
+| `enabled` | boolean | Start or stop sending `LOG` events. |
+
+With `enabled: true`, answer like `GET_LOGS` and then send new lines as `LOG` events until the
+server sends `enabled: false`. The server subscribes while someone has the agent's console open.
+
+### GET_INFO
+
+| Request | Answer |
+| --- | --- |
+| `{}` | `{ "version": "…", "info": { … } }`: the same diagnostics as `HELLO`'s `info`. The server stores them. |
+
+### CONFIG_SET
+
+Change settings stored on the agent. Only the fields present change.
+
+| Request field | Type | Description |
+| --- | --- | --- |
+| `server_url` | string | New server URL (`http://` or `https://`). |
+| `name` | string | Host name: letters, digits and `-`, at most 63 characters. |
+| `log_level` | `"error"` \| `"warn"` \| `"info"` \| `"debug"` | Log level. |
+| `ip` | object | `{ "mode": "dhcp" }`, or `{ "mode": "static", "address", "gateway", "netmask", "dns1"?, "dns2"? }` with IPv4 addresses. |
+
+Answer `{}` when the change was accepted. The ESP32 agent tries a new `server_url` or `ip` first
+and keeps it only if it reaches the server within 2 minutes.
+
+### REBOOT and FACTORY_RESET
+
+| Request | Answer |
+| --- | --- |
+| `{}` | `{}`, then restart. `FACTORY_RESET` erases Wi-Fi, server and credential first. The server revokes the agent's credential after a successful `FACTORY_RESET`. |
+
+### SSH_CHECK
+
+Log in to a device over SSH without opening a session, so no command (not even a forced one) runs.
+
+| Request field | Type | Description |
+| --- | --- | --- |
+| `host` | string | Host name or IP address. |
+| `port` | number | SSH port. |
+| `username` | string | User name. |
+| `auth` | object | `{ "type": "password", "password": "…" }` or `{ "type": "key", "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----…" }`. |
+| `host_key` | string \| null | The trusted host key fingerprint (`SHA256:…`), or `null` before it's trusted. If the device presents another key, stop before sending any credential. |
+
+| Answer field | Type | Description |
+| --- | --- | --- |
+| `online` | boolean | Whether the SSH port accepted the connection. |
+| `host_key` | string | The device's host key fingerprint, `SHA256:` plus unpadded base64 (as `ssh-keygen -l` shows it). |
+| `authenticated` | boolean | Whether the login worked. |
+| `error` | string | `host_key_mismatch`, `auth_failed` or a short message. |
+
+### SSH_EXEC
+
+Log in and run a fixed shutdown or reboot command. The request contains the `SSH_CHECK` fields
+(with a trusted `host_key`, never `null`) and:
+
+| Request field | Type | Description |
+| --- | --- | --- |
+| `action` | `"shutdown"` \| `"reboot"` | What to do. |
+| `os` | `"posix"` \| `"windows"` | Which command: `shutdown -h now` / `shutdown -r now`, or `shutdown /s /t 0` / `shutdown /r /t 0`. |
+| `sudo` | boolean | POSIX: prefix the command with `sudo -n `. |
+
+Answer `{}` when the command ran (or the connection dropped because the device went down), or
+`ok: false` with the error, for example `sudo: a password is required`. Never run anything else, and
+don't keep the credentials after the request.
+
+## Events
+
+### LOG
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `lines` | string[] | New log lines, at most 500 per event and 1024 characters per line. |
+| `dropped` | number | Optional: lines the agent dropped because its buffer was full. |
+
+### OTA_PROGRESS
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `version` | string | The version being installed. |
+| `state` | `"downloading"` \| `"installed"` \| `"failed"` | `installed`: the agent restarts next. |
+| `percent` | number | `0`–`100`. |
+| `error` | string | Optional: why it failed. |
+
+### HOST_STATUS
+
+Sent by the Linux agent after `HELLO` and every 5 minutes: `{ "hostname": "nas", "uptime_s": 86400 }`.
+The current server accepts and ignores it; the device's status comes from the connection itself.
+
+## Keep-alive and close codes
+
+The server sends `PING` every 30 seconds and closes a connection that sent nothing for 75 seconds.
+Agents should treat 100 seconds without any frame from the server as a dead connection and
+reconnect, with a backoff (the official agents: 5 seconds, doubling up to 60 seconds).
+
+| Code | Meaning | What the agent should do |
+| --- | --- | --- |
+| `1001` | The server is shutting down. | Reconnect. |
+| `4000` | Replaced: the same agent connected again. | Wait before reconnecting (the Linux agent waits 60 seconds); two copies may be running. |
+| `4001` | No valid `HELLO` within 10 seconds, wrong protocol or kind. | Fix the `HELLO`. |
+| `4002` | No message for 75 seconds. | Reconnect. |
+| `4003` | Credential revoked: enrolled again, revoked or reset in the dashboard. | Stop; enroll again. |
+| `4004` | The agent was deleted. | Stop. |
## Example agent
-A minimal agent for [Bun](https://bun.sh) that handles all three commands. Create an agent of type
-**Server** in the dashboard and fill in its credentials and your subnet's broadcast address:
+A minimal agent for [Bun](https://bun.sh) that wakes devices. In the dashboard, create an agent of
+the kind **Linux agent** (custom agents use that kind), click **Create setup command** and copy the
+token (`netignite_enroll_…`) from the command. Set your server and your subnet's broadcast address:
```ts [agent.ts]
+// bun run agent.ts (first run)
+// bun run agent.ts (afterwards)
import { createSocket } from "node:dgram";
+import { readFileSync, writeFileSync } from "node:fs";
-const SERVER_URL = "wss://netignite.example.com/api/agent-control-service";
-const AGENT_ID = "3";
-const AGENT_SECRET = "your-connection-secret";
-const BROADCAST_ADDRESS = "192.168.1.255";
+const SERVER = "https://netignite.example.com";
+const BROADCAST = "192.168.1.255";
+const CREDENTIAL_FILE = "./netignite-agent.json";
type Payload = Record;
-function sendMagicPacket(macAddress: string, port: number) {
- const mac = macAddress.split(/[:-]/).map((part) => Number.parseInt(part, 16));
- if (mac.length !== 6 || mac.some(Number.isNaN)) throw new Error("Invalid MAC address");
+async function enroll(token: string): Promise {
+ // The agent's own secret: 32 random bytes as 64 hex characters.
+ const secret = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex");
+ const response = await fetch(`${SERVER}/api/v1/agents/enroll`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ enrollment_token: token, secret, kind: "linux" }),
+ });
+ const body = await response.json();
+ if (!body.success) throw new Error(`Enrollment failed: ${body.message}`);
+
+ const credential = `netignite_agent_${body.data.token_id}:${secret}`;
+ writeFileSync(CREDENTIAL_FILE, JSON.stringify({ credential }), { mode: 0o600 });
+ return credential;
+}
+function sendMagicPacket(mac: string, port: number) {
+ const bytes = mac.split(":").map((part) => Number.parseInt(part, 16));
// 6 × 0xFF, then the MAC address 16 times.
const packet = Buffer.alloc(102, 0xff);
- for (let i = 0; i < 16; i++) Buffer.from(mac).copy(packet, 6 + i * 6);
+ for (let i = 0; i < 16; i++) Buffer.from(bytes).copy(packet, 6 + i * 6);
const socket = createSocket("udp4");
socket.bind(() => {
socket.setBroadcast(true);
- socket.send(packet, port, BROADCAST_ADDRESS, () => socket.close());
+ socket.send(packet, port, BROADCAST, () => socket.close());
});
}
-function handleCommand(command: string, payload: Payload): Payload {
- switch (command) {
- case "WAKEUP":
- sendMagicPacket(String(payload.macAddress), Number(payload.port));
- return { status: "OK", message: "Wakeup successful" };
- case "GET_STATUS":
- // Look up and ping the device here, then report "online" or "offline".
- return { status: "OK", message: "Status check not implemented", online: "unknown" };
- case "HEARTBEAT":
+function handle(cmd: string, p: Payload): Payload {
+ switch (cmd) {
+ case "PING":
+ return {};
+ case "WAKE":
+ sendMagicPacket(String(p.mac), Number(p.port));
return {};
default:
- return { status: "ERROR", message: "Unknown command" };
+ throw new Error(`Unknown command ${cmd}`);
}
}
-function connect() {
- const ws = new WebSocket(SERVER_URL, {
- headers: { Authorization: `Basic ${btoa(`${AGENT_ID}:${AGENT_SECRET}`)}` },
+function connect(credential: string) {
+ const ws = new WebSocket(`${SERVER.replace(/^http/, "ws")}/ws/agent`, {
+ headers: { Authorization: `Bearer ${credential}` },
});
- ws.onopen = () => console.log("Connected");
+ ws.onopen = () => {
+ const hello = { protocol: 2, kind: "linux", version: "0.1.0", capabilities: ["wake"] };
+ ws.send(JSON.stringify({ t: "evt", cmd: "HELLO", p: hello }));
+ };
ws.onmessage = (event) => {
- const message = String(event.data);
- const first = message.indexOf(":");
- const second = message.indexOf(":", first + 1);
- const command = message.slice(0, first);
- const id = message.slice(first + 1, second);
-
- let response: Payload;
+ const frame = JSON.parse(String(event.data));
+ if (frame.t !== "req") return;
try {
- response = handleCommand(command, JSON.parse(message.slice(second + 1)));
+ const p = handle(frame.cmd, frame.p ?? {});
+ ws.send(JSON.stringify({ t: "res", id: frame.id, ok: true, p }));
} catch (error) {
- response = { status: "ERROR", message: String(error) };
+ ws.send(JSON.stringify({ t: "res", id: frame.id, ok: false, err: String(error) }));
}
- ws.send(`${command}:${id}:${JSON.stringify(response)}`);
};
ws.onclose = (event) => {
- console.log(`Disconnected (${event.code} ${event.reason}), reconnecting in 10 seconds`);
- setTimeout(connect, 10_000);
+ if (event.code === 4003 || event.code === 4004) {
+ console.error(`Credential revoked or agent deleted (${event.code}); enroll again.`);
+ return;
+ }
+ console.log(`Disconnected (${event.code}), reconnecting in 10 seconds`);
+ setTimeout(() => connect(credential), 10_000);
};
}
-connect();
+const token = Bun.argv[2];
+const credential = token
+ ? await enroll(token)
+ : JSON.parse(readFileSync(CREDENTIAL_FILE, "utf8")).credential;
+connect(credential);
```
```bash [Terminal]
-bun run agent.ts
+bun run agent.ts netignite_enroll_…
```
-The agent's status in the dashboard turns **online** as soon as it connects.
+The agent shows up as **Online** in the dashboard. Choose it as the **Agent** of a device and
+**Power on** works. Status checks show *Unknown* until you add the `status.*` capabilities and
+answer `STATUS`.
diff --git a/content/docs/getting-started.md b/content/docs/getting-started.md
index be83cd6..d6c5335 100644
--- a/content/docs/getting-started.md
+++ b/content/docs/getting-started.md
@@ -1,6 +1,6 @@
---
title: "Getting Started"
-description: "Set up a self-hosted NetIgnite server, connect an ESP32 agent and wake your first device."
+description: "Set up a self-hosted NetIgnite server, install an ESP32 agent from the browser and wake your first device."
navigation:
title: Getting Started
---
@@ -10,21 +10,23 @@ navigation:
This guide takes you from an empty machine to your first remote wake-up. You need:
- a Linux host with [Docker](https://docs.docker.com/engine/install/) and Docker Compose for the
- server,
-- an ESP32 development board (`esp32dev`) and a USB cable,
+ server, and a host name with HTTPS for it (see [HTTPS & Reverse Proxy](/docs/server/reverse-proxy)),
+- an ESP32 development board (a classic ESP32, for example an *ESP32 DevKit*) and a USB data cable,
+- a computer with Google Chrome, Microsoft Edge or Firefox 151 or newer to install the agent,
- a device that supports Wake-on-LAN, in the same LAN as the ESP32.
::tip
Using the hosted app at [app.netignite.dev](https://app.netignite.dev/)? Skip steps 1 and 2, log
-in, and continue with step 3. Use `app.netignite.dev`, port `443` and SSL as the agent's server
-settings.
+in, and continue with step 3. No ESP32? A [Linux agent](/docs/agents/linux) on an always-on Linux
+machine in the same network works too.
::
::steps{level="3"}
### Start the server
-Create a `docker-compose.yml` and start it:
+Create a `docker-compose.yml`. Set `NETIGNITE_APP_URL` to the address you'll open the dashboard
+at:
```yaml [docker-compose.yml]
services:
@@ -34,6 +36,8 @@ services:
restart: unless-stopped
ports:
- "18232:18232"
+ environment:
+ NETIGNITE_APP_URL: https://netignite.example.com
volumes:
- /opt/netignite/data:/opt/netignite/data
- /opt/netignite/config:/opt/netignite/config
@@ -43,59 +47,59 @@ services:
docker compose up -d
```
-The server listens on port `18232`. See [Installation](/docs/server/installation) for details and
-other ways to run it.
+The server listens on port `18232`. Put it behind a reverse proxy with HTTPS: logging in and the
+ESP32 web installer only work over HTTPS (or on `localhost`). See
+[Installation](/docs/server/installation) and [HTTPS & Reverse Proxy](/docs/server/reverse-proxy).
-### Log in as admin
+### Set the admin password
-On first start, the server creates an `admin` account with a random password and writes it to
-`data/initial_admin_credentials`:
+On first start, the server creates the user `admin` and writes a link for setting its password to
+`config/initial_admin_password_reset_token.txt`:
```bash [Terminal]
-sudo cat /opt/netignite/data/initial_admin_credentials
+sudo cat /opt/netignite/config/initial_admin_password_reset_token.txt
```
-Open the dashboard and log in. The session cookie is only accepted over HTTPS or on `localhost`,
-so either open `http://localhost:18232` on the server itself or put the server behind
-[HTTPS](/docs/server/reverse-proxy) first. Then change the password under **Settings**.
+Open the link (it's valid for 7 days), choose a password and log in as `admin`. The welcome page
+suggests a few first steps; start with **Complete your profile** and enter your real email
+address. See [Users & Accounts](/docs/server/users).
### Create an agent
-Go to **Agents → Add New Agent**, enter a name, choose the type **Microcontroller** and save.
-The agent page now shows the **Connection ID** and the **Connection Secret**. You need both for
-the next step. Copy the secret right away: it's shown only once. See
+Go to **Agents → Add agent**, keep the kind **ESP32**, enter a name such as *Office ESP32* and click
+**Create agent**. The agent's page opens; its status is **Not set up**. See
[Managing Agents](/docs/guides/agents).
-### Flash the ESP32 agent
+### Install the ESP32 from the browser
-Clone the [Agent-ESP32](https://github.com/NetIgnite-project/Agent-ESP32) repository, copy
-`src/config/config.sample.h` to `src/config/config.h` and fill in your WiFi details, the server
-address, and the Connection ID and Secret:
+On the agent's page, click **Open the web installer** (or open the **Install** tab):
-```cpp [config.h]
-#define C_CONTROL_SERVER_HOST "netignite.example.com"
-#define C_CONTROL_SERVER_PORT (uint16_t) 443
-#define C_CONTROL_SERVER_USE_SSL (bool) true
+1. **Choose the firmware.** The recommended release is preselected.
+2. Click **Prepare installer**. The browser downloads the firmware from your server, and the
+ server adds the agent's setup data: your server's address and a single-use token, valid for 15
+ minutes.
+3. Plug the ESP32 into your computer, click **Connect and install** and pick its serial port.
+ After flashing, enter your Wi-Fi name and password in the dialog. They go straight to the
+ ESP32, not to NetIgnite.
+4. Wait a moment: the agent joins your Wi-Fi, connects to the server, and the page shows it as
+ connected.
-#define C_CONTROL_SERVER_AUTH_ID "1"
-#define C_CONTROL_SERVER_AUTH_SECRET "your-connection-secret"
-```
-
-Then build and flash the firmware with PlatformIO. The [ESP32 Agent](/docs/agents/esp32) page
-walks through every setting and command. When the agent shows as **online** in the dashboard, it
-is connected.
+Details and alternatives are on the [ESP32 Agent](/docs/agents/esp32) page.
### Add a device
-Go to **Devices → Add New Device** and enter a name, the device type, its **MAC address** (for
-example `AA:BB:CC:DD:EE:FF`), the Wake-on-LAN **port** (`9` unless you know otherwise) and the
-agent you just created. See [Devices & Wake-on-LAN](/docs/guides/devices).
+Go to **Devices → Add device** and enter a name, the device type, its **MAC address** (for example
+`AA:BB:CC:DD:EE:FF`), the **Wake-on-LAN port** (`9` unless you know otherwise) and the agent you
+just set up. Under **Status check**, choose **IPv4 address** and enter the device's IP address to
+see whether it's online. See [Devices](/docs/guides/devices).
### Power it on
-Shut the device down, then press **Power On** on the dashboard. The agent sends the magic packet
-and the device starts.
+Shut the device down, then click the power button next to it on the **Overview** or **Devices**
+page. The agent sends the magic packet and the device starts; its status turns **Online** a moment
+later.
::
-Nothing happened? Check [Troubleshooting](/docs/reference/troubleshooting).
+Nothing happened? Check [Troubleshooting](/docs/reference/troubleshooting). Next, you could
+[share the device](/docs/guides/sharing) or set it up for [shutdown over SSH](/docs/guides/ssh).
diff --git a/content/docs/guides/agents.md b/content/docs/guides/agents.md
index 59dc1f5..0bd1073 100644
--- a/content/docs/guides/agents.md
+++ b/content/docs/guides/agents.md
@@ -1,6 +1,6 @@
---
title: "Managing Agents"
-description: "Create NetIgnite agents, find their Connection ID and Secret, and understand agent status."
+description: "Create NetIgnite agents, set them up with the web installer or a setup command, use the ESP32 console and update the firmware over the network."
navigation:
title: Managing Agents
---
@@ -8,64 +8,135 @@ navigation:
# Managing Agents
An agent is the part of NetIgnite that lives inside your network. It keeps a WebSocket connection
-to the server and sends Wake-on-LAN packets for the devices assigned to it. You need one agent per
-network (broadcast domain) that contains devices you want to wake.
+to the server, wakes and checks devices, and shuts them down. You need one agent per network
+(broadcast domain) that contains devices you want to wake.
-## Agent types
+## Agent kinds
-| Type | Use it for |
+| Kind | Use it for |
| --- | --- |
-| **Microcontroller** | The [ESP32 agent](/docs/agents/esp32) or other embedded agents. |
-| **Server** | An agent running on an always-on computer, such as a [custom agent](/docs/agents/protocol). |
+| **ESP32** | A small board in your network, installed from the browser. Wakes devices, checks their status and logs in to them over SSH. See [ESP32 Agent](/docs/agents/esp32). |
+| **Linux agent** | A service for an always-on Linux machine, for example a Raspberry Pi or a server. Reports its own status and shuts the machine down or reboots it; in Wake-on-LAN mode it also wakes, checks and logs in to other devices. See [Linux Agent](/docs/agents/linux). |
-The type is informational. It doesn't change how the server talks to the agent.
+The kind can't be changed later. For an agent you [wrote yourself](/docs/agents/protocol), choose
+**Linux agent**.
## Creating an agent
-1. Open **Agents** and click **Add New Agent**.
-2. Enter a **Name**, choose the **Agent Type** and optionally add a **Description**.
-3. Click **Create Agent**. The server generates a random secret, and the agent's page opens.
+1. Open **Agents** and click **Add agent**.
+2. Choose the **Kind**, enter a **Name** and optionally a **Description**.
+3. Click **Create agent**. The agent's page opens with the status **Not set up**.
-The agent's page shows its **Connection Details**:
+Then set it up. The **Set up** card on the agent's **General** tab shows how.
-| Field | Agent setting |
-| --- | --- |
-| **Connection ID** | The agent's numeric ID, for example `3`. ESP32: `C_CONTROL_SERVER_AUTH_ID`. |
-| **Connection Secret** | A random 32-character secret. ESP32: `C_CONTROL_SERVER_AUTH_SECRET`. |
+## Setting up an agent
+
+Every setup uses a **single-use token** that is valid for **15 minutes**. The agent uses it once to
+register and generates its own secret; NetIgnite never shows that secret. Creating a new token
+invalidates older unused tokens of the agent. Setting an agent up again replaces its credential: a
+device that still uses the old one is disconnected.
+
+### ESP32: web installer
+
+Click **Open the web installer** or open the **Install** tab. The installer flashes the firmware
+over USB together with the agent's setup data (server address, name and token) and then asks for
+your Wi-Fi. It needs Google Chrome, Microsoft Edge or Firefox 151 or newer on a computer, and the
+dashboard must be opened over HTTPS. See
+[Install from the browser](/docs/agents/esp32#install-from-the-browser).
+
+### ESP32: serial console
+
+For a board that already runs the v2 firmware, click **Create setup command** under
+*Or over the serial console*. Connect to Wi-Fi in the board's serial console with
+`wifi "" ""`, then paste the command:
-::warning
-The secret is shown **only once**, right after the agent is created. Copy it with **Copy Secret**
-(or reveal it with the eye icon) before you leave the page. NetIgnite stores only a hash of it, so
-it can't be displayed again; if you lose it, [generate a new one](#changing-the-secret).
-::
+```text [Serial console]
+setup https://netignite.example.com netignite_enroll_…
+```
-Configure your agent with the two connection values and the server address. As soon as it
-connects, its status turns **online**. To change the name, type or description later, edit the
-fields and click **Update Agent**.
+See [Serial console](/docs/agents/esp32#serial-console).
+
+### Linux agent: setup command
+
+Click **Create setup command** and run the command on the Linux machine:
+
+```bash [Terminal]
+curl -fsSL https://netignite.example.com/install/linux.sh | sudo sh -s -- netignite_enroll_…
+```
+
+It downloads the agent, checks it, pairs it with the server and installs the systemd service. If
+the agent is already installed, open *Agent already installed?* for a
+`sudo netignite-agent pair …` command instead. See [Linux Agent](/docs/agents/linux).
## Agent status
| Status | Meaning |
| --- | --- |
-| `online` | The agent is connected to the server. |
-| `offline` | The agent is known but not connected. |
-| `unknown` | The server has no state for this agent. |
+| **Online** | The agent is connected. |
+| **Offline** | The agent is set up but not connected. **Last seen** shows when it was. |
+| **Not set up** | The agent has no credential yet, or it was revoked. |
+
+The **General** tab also shows the agent's version, its **capabilities** (what it can do, for
+example `wake`, `status.ip`, `ssh` or `host.control`) and the diagnostics it reports: IP
+addresses, Wi-Fi signal, uptime, memory and more. Agents that lose their connection reconnect on
+their own, after 5 seconds at first and at most every 60 seconds.
+
+The **Devices** card lists the devices that use the agent, to wake them or as the Linux agent on
+the device.
+
+## Console
+
+ESP32 agents with a v2 firmware that has the console get a **Console** tab:
+
+- **Log**: the agent's live log, starting with the lines it has stored (about the last 16 KB).
+ Choose the **log level** (*Errors*, *Warnings*, *Info*, *Debug*), turn **Follow** on or off, copy
+ or clear the view. The badge shows **Live**, **Agent offline** or **Disconnected**.
+- **Settings on the agent**:
+ - **Server address**: where the agent connects. Leave it empty to keep it.
+ - **Host name**: letters, digits and `-`; used after the next restart.
+ - **IP address**: *Automatic (DHCP)* or *Static* with address, gateway, netmask and DNS server.
+
+ Click **Send to agent**. A new server address or IP setting is only kept if the agent reaches the
+ server with it within 2 minutes; otherwise it restarts with the previous settings.
+- **Restart**: the agent is back after a few seconds.
+- **Factory reset**: erases Wi-Fi, server and credential on the agent and revokes the credential
+ in NetIgnite. Set it up again with the web installer.
+
+Wi-Fi can't be changed through the server. Use **Change Wi-Fi** in the web installer (see
+[ESP32 Agent](/docs/agents/esp32#change-wi-fi)) or the `wifi` command in the serial console. Only
+the agent's owner can open its console.
+
+## Firmware updates
+
+ESP32 agents whose firmware supports it (capability `ota`) are updated over the network. The
+**Firmware** card on the agent's **General** tab shows the installed version and, when the admin's
+[firmware channel](/docs/server/configuration#server-settings-in-the-dashboard) has a newer release,
+**Update to …**. Use **Other version** to install any synced release, also an older one. The agent
+must be online.
+
+An update goes through these steps, shown on the card:
+
+1. The agent downloads the firmware from your NetIgnite server with its own credential and checks
+ its size and SHA-256, and its signature if the agent's firmware requires signed updates.
+2. It writes the firmware to its second firmware slot and restarts into it.
+3. The new firmware is **on probation** until it has reached the server. If it crashes or doesn't
+ get there within 5 minutes, the board starts the previous firmware again, and the card reports
+ that the update was rolled back.
-The **Agents** page refreshes the status every 20 seconds. The **Ping** button refreshes it right
-away. An agent that loses its connection reconnects on its own (the ESP32 agent retries every 60
-seconds).
+The card then shows *Updated from … to …* or *Update to … failed* with the reason. An update that
+hasn't finished after 15 minutes counts as failed.
-## Changing the secret
+To update several agents, click **Update n agents** on the **Agents** page. It updates every online
+ESP32 agent with an available update, one after another: each update has to finish before the next
+one starts.
-Click **Regenerate Secret** on the agent's page to replace the secret, for example if you lost it
-or it may have leaked. The new secret is shown once. The old secret stops working right away and
-the agent is disconnected; it can only reconnect once it's configured with the new secret. For the
-ESP32 agent, that means rebuilding and reflashing the firmware.
+ESP32 boards with a firmware that can't be updated over the network show *This firmware can't be
+updated over the network*. Install a current firmware once with the web installer.
-Secrets are always generated by the server; you can't choose your own.
+## Revoking and deleting
-## Deleting an agent
+On the **General** tab, under **Danger Zone**:
-Delete an agent with the trash icon on the **Agents** page. A connected agent is disconnected
-immediately. Reassign or delete the devices that use it first: devices keep pointing at the
-deleted agent and can no longer be woken.
+- **Revoke credential** disconnects the agent. It can connect again only after it's set up again.
+- **Delete agent** disconnects and deletes it. Devices that used it keep their settings but have no
+ agent, so they can't be woken until you choose another one.
diff --git a/content/docs/guides/dashboard.md b/content/docs/guides/dashboard.md
index 4a9e489..5de5e4b 100644
--- a/content/docs/guides/dashboard.md
+++ b/content/docs/guides/dashboard.md
@@ -1,6 +1,6 @@
---
title: "Dashboard"
-description: "A tour of the NetIgnite web app: the device overview, device and agent management, and account settings."
+description: "A tour of the NetIgnite web app: overview, devices, agents, settings, API keys, the admin area and installing it as an app."
navigation:
title: Dashboard
---
@@ -8,43 +8,87 @@ navigation:
# Dashboard
The NetIgnite web app is the same on the [hosted app](https://app.netignite.dev/) and on a
-self-hosted server. The sidebar has three entries: **Home**, **Devices** and **Agents**. Your
-username at the bottom opens **Settings** and **Log out**.
+self-hosted server. It lives under `/dashboard`; the server's root address redirects there.
-## Home
+The sidebar has:
-The home page is an overview of all your devices:
+- **Overview**, **Devices** and **Agents**,
+- **Settings**: **General**, **Security** and **API Keys**,
+- **Admin** (admins only): **Users** and **Server Settings**,
+- **Documentation** (this site) and **API Reference** (the server's OpenAPI reference at
+ `/api/docs/v1`),
+- your name at the bottom, with **Settings** and **Log out**.
-- **Stat cards** with the number of devices that are online, offline, in an unknown state, and in
- total.
-- **Your Devices**: one card per device with its status and quick actions:
+The first time you log in, a welcome page suggests three steps: complete your profile, add an agent
+and add a device. Pick one or click **Skip for now**; it doesn't come back.
-| Action | What it does |
-| --- | --- |
-| **Power On** | Sends a Wake-on-LAN packet through the device's agent. Shown while the device isn't online. |
-| **Ping** | Refreshes the device's status right away. |
-| **Edit** | Opens the device's settings. |
+## Overview
+
+The overview shows your devices at a glance:
-Statuses refresh automatically every 20 seconds.
+- **Stat cards** with the number of devices that are online, offline and unknown, and how many of
+ your agents are online.
+- A warning when you have agents but none of them is online.
+- **Favorites**: your favorite devices with their status and power buttons. Without favorites, the
+ first eight devices are shown. Shared devices say who shared them.
+
+Everything refreshes every 20 seconds.
## Devices
-**Devices** lists all your devices in a table. Search by name or description, filter by status or
-type, and use the row actions to power on, ping, edit or delete a device. **Add New Device** opens
-the device form. See [Devices & Wake-on-LAN](/docs/guides/devices).
+**Devices** lists your devices and the devices shared with you. Search by name, filter by status or
+type, and use the row's buttons:
+
+| Column | Shows |
+| --- | --- |
+| Star | Add the device to your favorites or remove it. |
+| Name | Opens the device page. |
+| Status | Online, offline or unknown. Hover the badge for the reason and when it was checked. |
+| Access | Your role: owner, manager, operator or viewer, and who shared it. |
+| Power buttons | **Power on**, and a menu with **Shut down** and **Reboot** when the device supports them. Viewers don't see them. |
+
+A device page has the tabs **General** (status, power buttons, **Check now**, settings) and, for the
+owner, **Sharing** and **SSH**. See [Devices](/docs/guides/devices), [Sharing](/docs/guides/sharing)
+and [SSH](/docs/guides/ssh).
## Agents
-**Agents** lists your agents with their connection status. Search, filter by status or type, and
-ping, edit or delete agents. **Add New Agent** creates a new agent. See
-[Managing Agents](/docs/guides/agents).
+**Agents** lists your agents with their kind, status (**Online**, **Offline** or **Not set up**),
+firmware or agent version and when they were last seen. A badge shows when a firmware update is
+available or running. **Update n agents** updates every online ESP32 agent with an available
+update, one after another.
+
+An agent page has the tabs **General** (status, diagnostics, firmware, setup, settings) and, for
+ESP32 agents, **Console** and **Install**. See [Managing Agents](/docs/guides/agents).
## Settings
-**Settings** lets you change your username and password. See
-[Users & Accounts](/docs/server/users).
+- **General**: username, display name and email address.
+- **Security**: change your password or delete your account.
+- **API Keys**: keys for scripts and integrations.
+
+See [Users & Accounts](/docs/server/users).
+
+## Admin
+
+Admins see two more pages:
+
+- **Users**: create, edit and delete accounts and reset passwords.
+- **Server Settings**: public sign-up, the default ESP32 firmware channel and the firmware sync.
+
+See [Users & Accounts](/docs/server/users#managing-users) and
+[Configuration](/docs/server/configuration#server-settings-in-the-dashboard).
+
+## Installing as an app
+
+The dashboard can be installed as an app (PWA) on phones, tablets and computers. It then opens in
+its own window without the browser's address bar and gets an icon on the home screen or in the app
+list:
+
+- **Android (Chrome):** menu **⋮** → **Add to home screen**, then **Install**.
+- **iPhone and iPad (Safari):** **Share** → **Add to Home Screen**.
+- **Chrome or Edge on a computer:** the install icon at the right of the address bar.
-::note
-The **Shutdown** button appears for online devices, but remote shutdown isn't implemented yet. It
-only shows a notice. Marking devices as favorites is not saved yet either.
-::
+The app is the same dashboard and needs its server: without a connection it shows *You're offline*
+and a **Try again** button. Browsers only offer to install it over HTTPS (or on `localhost`), so a
+self-hosted server needs a [reverse proxy](/docs/server/reverse-proxy) with a certificate.
diff --git a/content/docs/guides/devices.md b/content/docs/guides/devices.md
index 8188db6..7bac239 100644
--- a/content/docs/guides/devices.md
+++ b/content/docs/guides/devices.md
@@ -1,15 +1,15 @@
---
-title: "Devices & Wake-on-LAN"
-description: "Add devices to NetIgnite, prepare them for Wake-on-LAN, power them on and check their status."
+title: "Devices"
+description: "Add devices to NetIgnite, prepare them for Wake-on-LAN, choose a status check, power them on, shut them down and mark favorites."
navigation:
- title: Devices & Wake-on-LAN
+ title: Devices
---
-# Devices & Wake-on-LAN
+# Devices
-A device is a machine you want to power on remotely. NetIgnite wakes it with a
+A device is a machine you want to control. NetIgnite wakes it with a
[Wake-on-LAN](https://en.wikipedia.org/wiki/Wake-on-LAN) magic packet that an agent in the same
-network broadcasts.
+network sends, checks whether it's online, and can shut it down or reboot it.
## Preparing a device
@@ -24,51 +24,91 @@ Wake-on-LAN is a hardware feature and usually disabled by default.
Fast Startup helps on many systems.
- Linux: check with `ethtool ` that `Wake-on` is `g`. Enable it with
`ethtool -s wol g` and make it persistent through your network configuration.
-3. **Wired connection.** Use Ethernet. Most computers can't be woken over WiFi.
+3. **Wired connection.** Use Ethernet. Most computers can't be woken over Wi-Fi.
4. **Note the MAC address** of the wired network card, for example with `ip link` (Linux),
`getmac /v` (Windows) or in your router's client list.
## Adding a device
-Open **Devices → Add New Device** and fill in:
+Open **Devices → Add device** and fill in:
| Field | Description |
| --- | --- |
| **Name** | A name to recognize the device by. |
-| **Device Type** | Server, Desktop PC, Laptop, Printer or NAS Storage. Only used for display. |
+| **Type** | Server, Desktop PC, Laptop, Printer, NAS or Other. Only used for display. |
| **Description** | Optional notes. |
-| **MAC Address** | The MAC address of the device's network card, as `AA:BB:CC:DD:EE:FF` or `AA-BB-CC-DD-EE-FF`. |
-| **Port** | The UDP port of the magic packet. Use `9` (the default) unless your device expects `7`. |
-| **Agent** | One of your agents, in the same network as the device. |
-
-MAC addresses must be six pairs of hex digits separated by `:` or `-`. Other notations such as
-`AABBCCDDEEFF` or `AABB.CCDD.EEFF` are rejected when you save the device.
+| **MAC address** | The MAC address of the device's wired network card, as `AA:BB:CC:DD:EE:FF` or `AA-BB-CC-DD-EE-FF`. It's stored with colons and in uppercase. |
+| **Wake-on-LAN port** | The UDP port of the magic packet. Use `9` (the default) unless the device expects `7`. |
+| **Agent** | The agent in the same network that wakes the device and runs its status check and SSH login: an ESP32, or a Linux agent in [Wake-on-LAN mode](/docs/agents/linux#wake-on-lan-mode). |
+| **Status check** | How NetIgnite finds out whether the device is online. See [below](#status-checks). |
+| **Address** | For the IPv4, IPv6 and SSH checks: the address the agent checks. |
+| **TCP port** | Optional for the MAC, IPv4 and IPv6 checks; the port for the SSH check (default `22`). |
+| **Linux agent on the device** | The [Linux agent](/docs/agents/linux#host-mode) running on this device, if any. It enables **Shut down** and **Reboot** and the status check *Linux agent on the device*. |
+
+Only the owner chooses the agents; the lists show only your own agents.
+
+## Status checks
+
+| Status check | What the agent does | Use it for |
+| --- | --- | --- |
+| **Off** | Nothing; the status stays *Unknown*. | Devices you only wake. |
+| **MAC (IPv6 link-local)** | Pings the IPv6 link-local address derived from the MAC address (EUI-64). A Linux agent also counts the device as online if its neighbor table (`ip neigh`) shows the MAC address as reachable. | Devices that use MAC-based IPv6 addresses. Doesn't work with Windows and many Linux defaults, which use random interface identifiers. |
+| **IPv4 address** | Pings the address. | Most devices. Give them a fixed address (DHCP reservation). |
+| **IPv6 address** | Pings the address. A zone such as `fe80::1%eth0` is allowed. | Devices with a stable IPv6 address. |
+| **SSH port** | Tries a TCP connection to port 22, or the **TCP port** you enter. Works with an IP address or a host name. | Linux, macOS and NAS devices with an SSH server. It doesn't log in. |
+| **Linux agent on the device** | Nothing: the device is online while its Linux agent is connected. | Machines that run the [Linux agent](/docs/agents/linux). |
+
+With **TCP port** set, the MAC, IPv4 and IPv6 checks also count the device as online when that port
+answers. That helps with devices that don't answer pings, such as Windows PCs with the default
+firewall: use `445` (file sharing) or `3389` (remote desktop). A refused connection also counts as
+online: the device answered.
+
+The address must be reachable from the agent's network, because the agent checks it, not the
+server. Checks run every 20 seconds; **Check now** on the device page checks right away. Hover the
+status badge to see when the device was checked and, for *Unknown*, why: for example *Agent is
+offline*, *No agent assigned* or *Status checks are turned off*.
## Powering on
-Click **Power On** on the dashboard or in the device list. NetIgnite reports:
+Click the power button on the **Overview**, in the **Devices** list or on the device page. NetIgnite
+shows *Wake-on-LAN packet sent* or *Couldn't power on* with the reason:
| Message | Meaning |
| --- | --- |
-| *Wakeup command sent* | The agent sent the magic packet. The device should start within a few seconds. |
+| *Wake-on-LAN packet sent* | The agent sent the magic packet. The device should start within a few seconds. |
+| *No agent is assigned to this device* | Choose an agent in the device's settings. |
| *Agent is offline* | The device's agent isn't connected. Check the agent first. |
-| *Failed to send wakeup command* | The agent didn't answer within 5 seconds. |
-| *Wakeup command failed: …* | The agent rejected the request, for example because of an invalid MAC address or port. |
+| *The agent didn't answer in time* | The agent didn't answer within 5 seconds. Try again. |
+| *The agent doesn't support this action* | The agent can't send Wake-on-LAN packets, for example a Linux agent without Wake-on-LAN mode. |
+| *The agent reported an error: …* | The agent couldn't send the packet, for example because it has no network. |
-*Wakeup command sent* means the packet left the agent. It doesn't confirm that the device turned
-on. Watch the device's status turn **online**, or check
+The button is disabled (*No online agent with Wake-on-LAN*) while no suitable agent is online.
+*Wake-on-LAN packet sent* means the packet left the agent. It doesn't confirm that the device
+turned on; watch its status turn **Online**, or see
[Troubleshooting](/docs/reference/troubleshooting#the-device-doesnt-wake-up).
-## Device status
+## Shutting down and rebooting
+
+When a device can be shut down, the power button gets a menu with **Shut down** and **Reboot**.
+NetIgnite asks for confirmation, then shows *Shutdown requested* or *Reboot requested*. This needs:
+
+- the **Linux agent on the device**, online (the preferred way for Linux machines: no login to the
+ device is stored anywhere), or
+- an **SSH login** with a trusted host key, and the device's agent online and able to log in over
+ SSH. Use a dedicated user that may only shut down and reboot. See [SSH](/docs/guides/ssh).
-NetIgnite asks the device's agent to ping the device. The ESP32 agent pings the device's IPv6
-link-local address, which it derives from the MAC address. See
-[How It Works](/docs/how-it-works#checking-device-status) for what the statuses mean and when this
-check reports a running device as `offline`.
+If both are set up, NetIgnite uses the Linux agent while it's online, and SSH otherwise.
-Statuses refresh every 20 seconds. The **Ping** button checks a device right away.
+## Favorites
+
+Click the star in the **Devices** list, or **Add to favorites** on the device page. Favorites appear
+on the **Overview**. They're personal: every user has their own.
## Editing and deleting
-Click **Edit** to change any field, including the agent. Delete a device with the trash icon in
-the device list; you'll be asked to confirm.
+Change the settings on the device's **General** tab and click **Save**. The owner and managers can
+edit a device; only the owner can change its agents and delete it. **Delete device** under
+**Danger Zone** removes the device and all its shares.
+
+Someone shared a device with you? **Leave device** on its page removes it from your account. See
+[Sharing](/docs/guides/sharing).
diff --git a/content/docs/guides/sharing.md b/content/docs/guides/sharing.md
new file mode 100644
index 0000000..72c06fd
--- /dev/null
+++ b/content/docs/guides/sharing.md
@@ -0,0 +1,63 @@
+---
+title: "Sharing"
+description: "Share NetIgnite devices with other users as viewer, operator or manager, and what each role can do."
+navigation:
+ title: Sharing
+---
+
+# Sharing
+
+You can share a device with other users on the same NetIgnite server: let your family power on the
+media PC, or let a colleague reboot a server. You stay the owner, and commands still go through
+your agent.
+
+## Roles
+
+Each share has a role:
+
+| | Owner | Manager | Operator | Viewer |
+| --- | --- | --- | --- | --- |
+| See the device and its status, **Check now** | ✓ | ✓ | ✓ | ✓ |
+| Add it to their favorites | ✓ | ✓ | ✓ | ✓ |
+| **Power on**, **Shut down**, **Reboot** | ✓ | ✓ | ✓ | |
+| Edit name, type, description, MAC address, port and status check | ✓ | ✓ | | |
+| Choose the agent and the Linux agent on the device | ✓ | | | |
+| Set up the SSH login | ✓ | | | |
+| Share the device, change and remove shares | ✓ | | | |
+| Delete the device | ✓ | | | |
+
+Agents are never shared. Users you share a device with see the name and status of its agents, but
+can't open them, use them for their own devices or see their credentials. They never see the
+device's SSH login either.
+
+## Sharing a device
+
+1. Open the device and its **Sharing** tab. Only the owner has it.
+2. Enter the other user's **username or email address** and choose a role.
+3. Click **Share**.
+
+The user must already have an account on the server; NetIgnite doesn't send invitations. The device
+then appears in their **Devices** list with their role and *by* your name, and on their
+**Overview** as *shared by* you.
+
+Under **People with access**, change someone's role with the role menu, or remove their access with
+the **×** button. Removing a share also removes the device from their favorites.
+
+## Leaving a shared device
+
+On a device someone shared with you, the **General** tab shows your role and a **Leave device**
+section. **Leave** removes the device from your account until the owner shares it again.
+
+## When the owner deletes things
+
+- Deleting the device removes all its shares.
+- Deleting or changing the device's agent affects everyone: if the agent is gone, nobody can wake
+ the device until the owner chooses another one.
+- Deleting the owner's account deletes their devices, and with them all shares.
+
+## With the API
+
+The owner manages shares with the [REST API](/docs/reference/api#sharing):
+`GET /api/v1/devices//shares`, `POST` with `{ "user": "", "role": "operator" }`,
+`PUT /api/v1/devices//shares/` with a new role, and `DELETE` to remove a share. A user
+leaves a device with `DELETE /api/v1/devices//shares/`.
diff --git a/content/docs/guides/ssh.md b/content/docs/guides/ssh.md
new file mode 100644
index 0000000..9120f30
--- /dev/null
+++ b/content/docs/guides/ssh.md
@@ -0,0 +1,175 @@
+---
+title: "SSH"
+description: "Shut down and reboot devices over SSH: why the Linux agent is the safer choice, a dedicated user with a generated Ed25519 key that may only shut down and reboot, and trusting the host key."
+navigation:
+ title: SSH
+---
+
+# SSH
+
+For devices that can't run the [Linux agent](/docs/agents/linux) (many NAS systems, a Mac, a BSD
+box, a Windows PC with OpenSSH Server), NetIgnite can shut them down and reboot them over SSH. The
+**device's agent** logs in, because it's the one inside the LAN; the server stores the login and
+sends it to that agent for each command.
+
+::warning
+**Prefer the Linux agent wherever it runs.** For a Linux machine with systemd, install the
+[Linux agent](/docs/agents/linux) on it and choose it as the device's **Linux agent** instead of
+setting up SSH. It can only shut down and reboot that machine (through polkit), and no login to the
+machine is stored anywhere. With SSH, the server stores a login to the device: whoever gets the
+server's database **and** its secrets key can log in to the device with it. If you use SSH, log in
+as a [dedicated user](#a-dedicated-user) that may only shut down and reboot, with a generated key.
+Never use `root`, an administrator or your own account (Windows is the exception that needs an
+administrator, see [Windows](#windows)).
+::
+
+What NetIgnite does over SSH is fixed:
+
+| | Linux, macOS, BSD | with *Use sudo* | Windows (OpenSSH) |
+| --- | --- | --- | --- |
+| Shut down | `shutdown -h now` | `sudo -n shutdown -h now` | `shutdown /s /t 0` |
+| Reboot | `shutdown -r now` | `sudo -n shutdown -r now` | `shutdown /r /t 0` |
+
+There's no way to run anything else: no shell, no file access, no other commands.
+
+## Requirements
+
+- The device's **Agent** (on its **General** tab) must be able to log in over SSH and be online: an
+ ESP32 agent or a Linux agent in [Wake-on-LAN mode](/docs/agents/linux#wake-on-lan-mode), in the
+ device's network. The agent's capabilities on its page include `ssh` then.
+- An SSH server on the device, reachable from the agent.
+- You must be the device's owner. The **SSH** tab isn't available to users the device is shared
+ with.
+
+## A dedicated user
+
+The stored login should be able to do as little as possible on the device, so that a leaked login
+allows nothing but a shutdown:
+
+- **A user only for NetIgnite**, for example `netignite`. Not `root`, not your own account.
+- **No other rights.** Don't add it to `sudo`, `wheel`, `admin` or any other group with extra
+ rights. The sudoers line from **Prepare the device** allows it exactly the two `shutdown`
+ commands.
+- **A generated key, not a password.** The `authorized_keys` line of a generated key lets only the
+ two commands through. A password works for everything the account can do, including a shell.
+
+On Linux, create the user with:
+
+```sh
+sudo useradd --create-home --shell /bin/sh netignite
+sudo -u netignite mkdir -m 700 /home/netignite/.ssh
+```
+
+Keep a real shell such as `/bin/sh` (not `nologin`): SSH runs the forced command through the user's
+shell. Then add the `authorized_keys` and sudoers lines as described
+[below](#linux-macos-bsd). On macOS, create a **Standard** user (not an administrator) in
+**System Settings → Users & Groups** and allow it under **Remote Login**; on BSD, use `adduser`.
+
+For Linux, macOS and BSD, the dashboard warns when the username is `root`, `admin` or `Administrator`.
+
+## Setting up the login
+
+Open the device and its **SSH** tab:
+
+| Field | Description |
+| --- | --- |
+| **Host** | Host name or IP address. Empty: the device's status check address. |
+| **Port** | Usually `22`. |
+| **Username** | The account the agent logs in with: a [dedicated user](#a-dedicated-user), for example `netignite`. |
+| **Operating system** | *Linux, macOS, BSD* or *Windows (OpenSSH)*. Decides the command (table above). |
+| **Use sudo** | Linux, macOS, BSD: run `shutdown` with `sudo -n`. Not needed for `root`. |
+| **Authentication** | **New key pair** (recommended): the server generates an Ed25519 key pair. **Password**: the account's password. **Keep the current**: change the other fields only. |
+
+Click **Save login**. With a generated key, the **Prepare the device** card shows what to add on
+the device.
+
+### Linux, macOS, BSD
+
+1. Add the line from **Prepare the device** to `~/.ssh/authorized_keys` of the user. It looks
+ like this (with *Use sudo*):
+
+ ```text [authorized_keys]
+ restrict,command="case \"$SSH_ORIGINAL_COMMAND\" in 'sudo -n shutdown -h now'|'sudo -n shutdown -r now') exec $SSH_ORIGINAL_COMMAND ;; *) echo 'Only shutdown and reboot are allowed' >&2; exit 1 ;; esac" ssh-ed25519 AAAA… netignite-device-7
+ ```
+
+ `restrict` turns off port forwarding, agent forwarding and a terminal. The forced `command` lets
+ only the two commands through and refuses anything else, so the key is useless for anything but
+ shutdown and reboot. **Test login** doesn't open a session, so the forced command doesn't run
+ for it. You can add `from=""` after `restrict,` to accept the key only
+ from the agent.
+
+2. With *Use sudo*, allow the two commands without a password. Run
+ `sudo visudo -f /etc/sudoers.d/netignite` and add the line from **Prepare the device**:
+
+ ```text [sudoers]
+ netignite ALL=(root) NOPASSWD: /usr/sbin/shutdown -h now, /usr/sbin/shutdown -r now, /sbin/shutdown -h now, /sbin/shutdown -r now
+ ```
+
+ `sudo -n` fails at once instead of asking for a password, so a missing line shows up in the
+ dashboard as `sudo: a password is required`.
+
+With a password instead of a key, there's no forced command: the account can do whatever its
+password allows. Prefer a key.
+
+### Windows
+
+Install **OpenSSH Server** (Settings → System → Optional features), start the `sshd` service and
+choose *Windows (OpenSSH)* as the operating system. Shutting down needs an administrator account,
+so on Windows the login can't be limited to shutdown and reboot: whoever gets it controls the PC.
+Keep the risk small:
+
+- Create a **separate administrator account only for NetIgnite** instead of using your own or the
+ built-in `Administrator`.
+- Use a **generated key** rather than a password, and accept it only from the agent: put
+ `restrict,from=""` at the start of its line.
+
+Where the login goes:
+
+- **Key:** put the line from **Prepare the device** (`restrict ssh-ed25519 AAAA…`) into
+ `C:\ProgramData\ssh\administrators_authorized_keys`. Only Administrators and SYSTEM may have
+ access to that file. Windows' OpenSSH runs forced commands through `cmd.exe`, so this line has no
+ forced command.
+- **Password:** use the account's password.
+
+## Trusting the host key
+
+NetIgnite trusts a device's SSH host key on first use, after you've checked it:
+
+1. In the **Host key** card, click **Test login**. The agent connects, checks the login without
+ running anything and reports the device's host key fingerprint (`SHA256:…`).
+2. Compare it with the key on the device, for example
+ `ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`.
+3. Click **Trust this host key**.
+
+Shutdown and reboot over SSH work from then on: the power button of the device gets **Shut down**
+and **Reboot**. Every later login checks the key first and stops before sending the password or
+key if it differs. Changing the login's host or port removes the trusted key; test and trust again.
+
+| Test result | Meaning |
+| --- | --- |
+| *Login works* | The agent logged in. Trust the key if you haven't. |
+| *The device didn't answer on its SSH port* | Wrong host or port, the device is off, or a firewall blocks it. |
+| *Login failed*, `auth_failed` | Wrong username, password, or `authorized_keys` line. |
+| *Login failed*, `host_key_mismatch` | The device presents another key than the trusted one. If you reinstalled the device, trust the new key; otherwise someone may be intercepting the connection. |
+
+## Security
+
+- **A stored login is a key to the device.** That's why the [Linux agent](/docs/agents/linux) is the
+ better choice for Linux machines, and why the login should belong to a
+ [dedicated user](#a-dedicated-user) that may only shut down and reboot.
+- **Write-only.** The API never returns a stored password or private key. The dashboard only shows
+ the public key.
+- **Encrypted at rest.** Passwords and private keys are encrypted with the server's secrets key
+ (`NETIGNITE_SECRETS_KEY` or `config/secrets.key`). Back up the key with the database; without it,
+ you have to set the logins up again.
+- **Only to the device's agent.** The login is sent over the agent's TLS WebSocket for each test or
+ command, kept in memory only for that login and wiped afterwards. The agent never stores or logs
+ it.
+- **Fixed commands, trusted host key.** Commands only run on a device whose host key you trusted.
+- **Owner only.** Users the device is shared with can use **Shut down** and **Reboot** (operators
+ and managers) but can't see or change the login.
+
+## Removing the login
+
+**Remove SSH login** deletes the stored password or key. Remove the `authorized_keys` line and the
+sudoers file from the device too.
diff --git a/content/docs/how-it-works.md b/content/docs/how-it-works.md
index c605d59..b8a25dd 100644
--- a/content/docs/how-it-works.md
+++ b/content/docs/how-it-works.md
@@ -1,6 +1,6 @@
---
title: "How It Works"
-description: "The building blocks of NetIgnite (server, agents and devices) and what happens when you wake a device or check its status."
+description: "The building blocks of NetIgnite (server, agents and devices) and what happens when you power on, check, shut down or share a device."
navigation:
title: How It Works
---
@@ -8,79 +8,118 @@ navigation:
# How It Works
NetIgnite has three building blocks: the **server** you log in to, **agents** that live in your
-networks, and the **devices** you want to power on.
+networks, and the **devices** you want to control.
```text
- Browser ──HTTPS──▶ NetIgnite Server ◀──WebSocket── Agent ──Wake-on-LAN / ping──▶ Devices
- (dashboard + API) (outbound (ESP32 in (same LAN as the agent)
- from the LAN) your LAN)
+ Browser ──HTTPS──▶ NetIgnite Server ◀──WebSocket── Agent ──Wake-on-LAN / ping / SSH──▶ Devices
+ (dashboard, API, (outbound (ESP32 or (same LAN as the agent)
+ agent gateway) from the LAN) Linux agent)
```
## Concepts
-**Server.** A [Nuxt](https://nuxt.com) application running on [Bun](https://bun.sh). It serves the
-web dashboard and the [REST API](/docs/reference/api), and it accepts agent connections on
-`/api/agent-control-service`. All of this uses one port (`18232` in the Docker image). Users,
-agents and devices are stored in a SQLite database.
+**Server.** A [Nuxt](https://nuxt.com) application with a [Hono](https://hono.dev) API, running on
+[Bun](https://bun.sh) and storing its data in SQLite. It serves the web dashboard, the
+[REST API](/docs/reference/api) under `/api/v1`, and the WebSocket endpoints `/ws/agent` (agents)
+and `/ws/console` (the live log of an ESP32 agent). Everything uses one port: `18232`.
**Agent.** A program inside your network that connects *out* to the server over a WebSocket and
waits for commands. Because the agent dials out, you don't need port forwarding or a VPN into the
-network you want to control. Every agent has a numeric **ID** and a **secret**; it authenticates
-with both when it connects. The [ESP32 agent](/docs/agents/esp32) is the reference
-implementation, and you can [write your own](/docs/agents/protocol).
+network you want to control. There are two kinds:
-**Device.** A machine you want to wake: a desktop PC, server, laptop, printer or NAS. A device
-record stores the device's **MAC address**, the **Wake-on-LAN port** (usually `9`) and the
-**agent** that is responsible for it. The agent must be in the same local network (broadcast
-domain) as the device.
+| Kind | Runs on | Does |
+| --- | --- | --- |
+| [ESP32](/docs/agents/esp32) | An ESP32 board in your LAN | Wakes and checks devices in its network, logs in to them over SSH, updates itself over the network. |
+| [Linux agent](/docs/agents/linux) | A Linux machine with systemd | Reports the machine's own status and shuts it down or reboots it. In Wake-on-LAN mode, also wakes, checks and logs in to other devices in its network. |
-Agents and devices belong to the user who created them. Each user only sees their own.
+You can also [write your own agent](/docs/agents/protocol).
+
+**Device.** A machine you want to control: a desktop PC, server, laptop, printer or NAS. A device
+record stores the device's **MAC address**, the **Wake-on-LAN port** (usually `9`), the **agent**
+that wakes it, how its **status is checked**, and optionally the **Linux agent running on it** and
+an **SSH login**. The agent must be in the same local network (broadcast domain) as the device.
+
+Agents and devices belong to the user who created them. Agents are never shared; devices can be
+[shared](/docs/guides/sharing) with other users.
+
+## Connecting an agent
+
+An agent proves who it is with a credential that only the agent knows:
+
+1. In the dashboard, the agent's owner creates a **single-use enrollment token**
+ (`netignite_enroll_…`, valid for 15 minutes). The ESP32 web installer writes it into the
+ board's flash; for the Linux agent it's part of the setup command.
+2. The agent generates a random **secret** itself and sends it with the token to
+ `POST /api/v1/agents/enroll`. The server stores only a hash of the secret and answers with a
+ token ID.
+3. From then on, the agent connects to `/ws/agent` with
+ `Authorization: Bearer netignite_agent_:`. Its first message is a `HELLO`
+ that lists its **capabilities**; the server only sends commands the agent listed.
+
+The secret never leaves the agent and is never shown. Enrolling again (with a new token) replaces
+the old credential. See [Agent Protocol](/docs/agents/protocol) for the details.
## Waking a device
-1. You press **Power On** in the dashboard. The browser calls
- `POST /api/devices//wakeup`.
-2. The server looks up the device and its agent. If the agent isn't connected, the request fails
- with **Agent is offline**.
-3. The server sends a `WAKEUP` command with the device's MAC address and port to the agent.
-4. The agent broadcasts a Wake-on-LAN magic packet to its subnet (the ESP32 agent sends it three
- times, 100 ms apart) and replies `OK`.
+1. You press **Power on**. The browser calls `POST /api/v1/devices//power-on`.
+2. The server checks that you may power the device on (owner, manager or operator) and that its
+ agent is online and supports Wake-on-LAN. Otherwise it answers with an error such as
+ *Agent is offline*.
+3. The server sends the agent a `WAKE` command with the device's MAC address and port.
+4. The agent broadcasts the magic packet three times, 100 ms apart, to its subnet's broadcast
+ address and to `255.255.255.255`, and answers.
5. The device's network card receives the packet and powers the machine on.
-The agent's `OK` means that the packet was sent, not that the device actually started.
-Wake-on-LAN must be enabled in the device's BIOS/UEFI and network driver. See
+*Wake-on-LAN packet sent* means that the packet was sent, not that the device started.
+Wake-on-LAN must be enabled on the device. See
[Troubleshooting](/docs/reference/troubleshooting#the-device-doesnt-wake-up) if nothing happens.
## Checking device status
-The dashboard refreshes device and agent status every 20 seconds.
+The server checks every device with a status check every **20 seconds** and keeps the results;
+the dashboard reads them and refreshes every 20 seconds too. Checks through the same agent run one
+after another, different agents work in parallel. Each check has 6 seconds. **Check now** on a
+device page runs one immediately (at most every 5 seconds per device).
-- **Agent status** is `online` while the agent's WebSocket connection is open. The server keeps
- this state in memory and sends every connected agent a `HEARTBEAT` once a minute.
-- **Device status** comes from the agent. For each device, the server sends a `GET_STATUS`
- command with the device's MAC address. The ESP32 agent derives the device's IPv6 link-local
- address from the MAC (the EUI-64 address) and pings it once.
+| Status check | How it works |
+| --- | --- |
+| Off | No check. The status stays *Unknown*. |
+| MAC (IPv6 link-local) | The agent pings the IPv6 link-local address derived from the MAC address (EUI-64). |
+| IPv4 address / IPv6 address | The agent pings the address you enter. |
+| SSH port | The agent tries a TCP connection to port 22 (or the port you enter). |
+| Linux agent on the device | The device is online while the Linux agent running on it is connected. |
+
+For the first three, you can add a **TCP port** (for example `445` or `3389`): the device then also
+counts as online when that port answers. See [Devices](/docs/guides/devices#status-checks) for
+when to use which.
| Status | Meaning |
| --- | --- |
-| `online` | The device answered the ping. |
-| `offline` | The device didn't answer within one second. |
-| `unknown` | The agent is offline, didn't answer within 5 seconds, or rejected the request (for example because of a malformed MAC address). |
+| Online | The device answered. |
+| Offline | The device didn't answer. |
+| Unknown | No check is configured, or the check couldn't run: no agent, the agent is offline, didn't answer in time or doesn't support the check. The badge's tooltip shows the reason. |
+
+## Shutting down and rebooting
+
+Shutdown and reboot need one of two things:
+
+- **The Linux agent on the device** (preferred). The server sends it `HOST_CONTROL`, and the agent
+ runs `systemctl poweroff` or `systemctl reboot`. It can't do anything else, and the server holds
+ no login to the device.
+- **An SSH login.** The device's agent logs in to the device over SSH and runs a fixed `shutdown`
+ command. The device's host key must have been trusted first. The server stores the login, so it
+ should belong to a user that may only shut down and reboot. See [SSH](/docs/guides/ssh).
-::warning
-The EUI-64 status check only works when the device has IPv6 enabled, uses a MAC-derived
-link-local address and answers ICMPv6 echo requests. Windows and many modern Linux distributions
-use randomized IPv6 interface identifiers by default, so such devices show as `offline` even when
-they are running. Waking them is not affected.
-::
+If a device has both, the server uses the Linux agent while it's online.
## Where data lives
| Data | Storage |
| --- | --- |
-| Users, agents, devices | SQLite database `data/db.sqlite` |
-| Login sessions | Server memory (a restart logs everyone out) |
-| Agent connection state | Server memory |
-| Server settings | `config/config.json` |
+| Users, sessions, API keys, agents, devices, shares, favorites, SSH logins, the firmware list | SQLite database `data/netignite.sqlite` |
+| Agent credentials, session tokens, API keys | Only as hashes in the database |
+| SSH passwords and private keys | Encrypted in the database with the secrets key (`config/secrets.key` or `NETIGNITE_SECRETS_KEY`) |
+| ESP32 firmware releases | `data/firmware//`, downloaded from GitHub |
+| Agent connections, device status, firmware update progress | Server memory: agents reconnect and statuses are checked again after a restart |
Continue with [Getting Started](/docs/getting-started) to set up your own instance.
diff --git a/content/docs/index.md b/content/docs/index.md
index f980369..81628b3 100644
--- a/content/docs/index.md
+++ b/content/docs/index.md
@@ -1,36 +1,48 @@
---
title: "Documentation"
-description: "Learn how NetIgnite powers on devices from anywhere: self-host the server, connect an agent and wake your devices."
+description: "Learn how NetIgnite powers on, checks and shuts down devices from anywhere: self-host the server, set up an agent and control your devices."
navigation:
title: Overview
---
# NetIgnite Documentation
-NetIgnite lets you power on computers, servers and other devices in your network from anywhere. A
-small **agent** inside your network keeps a connection open to the **NetIgnite server**. When you
-press **Power On** in the dashboard, the server tells the agent to send a
+NetIgnite lets you power on computers, servers and other devices in your network from anywhere,
+see whether they're online, and shut them down or reboot them. A small **agent** inside your
+network keeps a connection open to the **NetIgnite server**. When you press **Power on** in the
+dashboard, the server tells the agent to send a
[Wake-on-LAN](https://en.wikipedia.org/wiki/Wake-on-LAN) magic packet to the device.
::note
-NetIgnite is under active development. These docs describe the latest versions of the
-[server](https://github.com/NetIgnite-project/Server/releases) and the
-[ESP32 agent](https://github.com/NetIgnite-project/Agent-ESP32). Where a feature isn't available
-yet, the page says so.
+These docs describe **NetIgnite v2**: the server 2.x, the ESP32 firmware 2.x and the Linux agent
+1.x. v1 agents can't connect to a v2 server. Coming from v1? Read
+[Upgrading from v1](/docs/server/upgrading).
::
## Components
| Component | What it does | Source |
| --- | --- | --- |
-| **Server** | Web dashboard, REST API and the WebSocket service that agents connect to. Stores users, agents and devices in SQLite. | [NetIgnite-project/Server](https://github.com/NetIgnite-project/Server) |
-| **ESP32 Agent** | Firmware for an ESP32 that sits in your LAN, sends Wake-on-LAN packets and checks whether devices are online. | [NetIgnite-project/Agent-ESP32](https://github.com/NetIgnite-project/Agent-ESP32) |
+| **Server** | Web dashboard, REST API and the WebSocket gateway that agents connect to. Stores users, agents and devices in SQLite. | [NetIgnite-project/Server](https://github.com/NetIgnite-project/Server) |
+| **ESP32 agent** | Firmware for an ESP32 board in your LAN. Wakes devices, checks whether they're online and can shut them down over SSH. Installed from the browser and updated over the network. | [NetIgnite-project/Agent-ESP32](https://github.com/NetIgnite-project/Agent-ESP32) |
+| **Linux agent** | A systemd service for a Linux machine. Reports the machine's status and shuts it down or reboots it; optionally wakes and checks other devices in its network, like the ESP32 agent. | [NetIgnite-project/Agent-Linux](https://github.com/NetIgnite-project/Agent-Linux) |
| **Website** | This site, including the docs. | [NetIgnite-project/Website](https://github.com/NetIgnite-project/Website) |
You can use the hosted app at [app.netignite.dev](https://app.netignite.dev/) or run your own
server. All components are open source under the
[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html).
+## What you can do
+
+- **Power on** devices with Wake-on-LAN through an agent in their network.
+- **Check** whether a device is online: by its IPv4 or IPv6 address, a TCP port, its MAC address
+ (IPv6 link-local), its SSH port, or the Linux agent running on it.
+- **Shut down and reboot** devices through the Linux agent on the device, or over SSH.
+- **Share** devices with other users as viewer, operator or manager.
+- **Install the ESP32 agent from the browser** and keep it up to date with over-the-air updates.
+- **Automate** with the REST API and API keys.
+- **Use it on your phone**: install the dashboard as an app (PWA).
+
## Where to start
::icon-card-grid
@@ -38,7 +50,7 @@ server. All components are open source under the
cards:
- icon: i-lucide-workflow
title: How It Works
- description: Server, agents and devices, and what happens when you press Power On.
+ description: Server, agents and devices, and what happens when you press Power on.
to: /docs/how-it-works
- icon: i-lucide-rocket
title: Getting Started
@@ -50,15 +62,23 @@ cards:
to: /docs/server/installation
- icon: i-lucide-cpu
title: ESP32 Agent
- description: Configure, build and flash the agent firmware.
+ description: Install the firmware from the browser, its console and updates.
to: /docs/agents/esp32
+ - icon: i-lucide-terminal
+ title: Linux Agent
+ description: Install the agent as a systemd service with one command.
+ to: /docs/agents/linux
+ - icon: i-lucide-history
+ title: Upgrading from v1
+ description: Import your v1 data and set your agents up again.
+ to: /docs/server/upgrading
- icon: i-lucide-cable
title: Agent Protocol
description: The WebSocket protocol, if you want to write your own agent.
to: /docs/agents/protocol
- icon: i-lucide-braces
title: REST API
- description: Every HTTP endpoint of the server.
+ description: Authentication, API keys and the most important endpoints.
to: /docs/reference/api
---
::
diff --git a/content/docs/reference/api.md b/content/docs/reference/api.md
index b342acc..19eb8e8 100644
--- a/content/docs/reference/api.md
+++ b/content/docs/reference/api.md
@@ -1,6 +1,6 @@
---
title: "REST API"
-description: "Reference for the NetIgnite server's HTTP API: authentication, accounts, agents, devices and Wake-on-LAN."
+description: "The NetIgnite server's HTTP API: authentication with logins and API keys, the response envelope, the OpenAPI reference and the most important endpoints."
navigation:
title: REST API
---
@@ -8,290 +8,275 @@ navigation:
# REST API
The dashboard is built on the server's JSON API, and you can call the same endpoints from scripts
-and automations. All paths are relative to your server, for example
-`https://netignite.example.com/api/devices`.
+and automations. The API lives under `/api/v1` on your server, for example
+`https://netignite.example.com/api/v1/devices`.
-## Basics
+::tip
+Every server includes a complete, interactive **OpenAPI reference** at `/api/docs/v1` (the
+dashboard links it as **API Reference**), with all request and response schemas. The OpenAPI
+document itself is at `/api/docs/v1/openapi`. Admins can turn both off with
+`NETIGNITE_API_DISABLE_DOCS=true`. This page covers the basics and the most important endpoints.
+::
-### Authentication
+## Authentication
+
+Requests authenticate with a bearer token:
+
+```http
+Authorization: Bearer
+```
+
+| Token | Looks like | Get it | Valid |
+| --- | --- | --- | --- |
+| **API key** | `netignite_apikey_…` | **Settings → API Keys** in the dashboard | Until it expires or is deleted |
+| **Login (session)** | `netignite_sess_…` | `POST /api/v1/auth/login` | 7 days, or until logout |
-The API uses the same session cookie as the dashboard. Log in with
-[`POST /api/auth/login`](#log-in) and send the returned `session` cookie with every request.
-API keys aren't available yet.
+Use an API key for scripts. It works for agents, devices, firmware and (for admins) the admin
+endpoints. The endpoints under `/account` (your profile, password and API keys) and
+`/auth/session` and `/auth/logout` need a login token.
```bash [Terminal]
-# Log in and store the session cookie
-curl -c cookies.txt -H "Content-Type: application/json" \
- -d '{"username":"admin","password":""}' \
- https://netignite.example.com/api/auth/login
+curl -H "Authorization: Bearer netignite_apikey_…" \
+ https://netignite.example.com/api/v1/devices
+
+curl -X POST -H "Authorization: Bearer netignite_apikey_…" \
+ https://netignite.example.com/api/v1/devices/4/power-on
+```
-# Use the cookie
-curl -b cookies.txt https://netignite.example.com/api/devices
-curl -b cookies.txt -X POST https://netignite.example.com/api/devices/1/wakeup
+To get a login token instead:
+
+```bash [Terminal]
+curl -H "Content-Type: application/json" \
+ -d '{"username":"admin","password":""}' \
+ https://netignite.example.com/api/v1/auth/login
```
-Every endpoint except those under `/api/auth/` requires a session. Without one, the server answers
-`401` with `No session found` or `Invalid session`. Sessions last 30 days and end when the server
-restarts.
+The answer's `data.token` is the login token. After 5 failed logins for a username from one address
+(15 from all addresses), the server answers `429` for 5 minutes.
-### Responses
+## Responses
-Responses share one envelope:
+Every response uses the same envelope:
```json
{
- "status": "OK",
- "message": "Devices retrieved successfully.",
+ "success": true,
+ "code": 200,
+ "message": "Devices retrieved",
"data": []
}
```
| Field | Description |
| --- | --- |
-| `status` | `"OK"` on success, `"ERROR"` on failure. |
-| `message` | Human-readable result. Missing on the status endpoints. |
-| `data` | The result, if any. |
+| `success` | `true` or `false`. |
+| `code` | The HTTP status code. |
+| `message` | Human-readable result. The dashboard shows error messages as they are. |
+| `data` | The result; `null` if there is none. Missing on errors. |
-Errors use the matching HTTP status code: `400` for invalid input, `401` without a valid session,
-`404` for agents or devices that don't exist or belong to another user, `409` for conflicts,
-`503` when an agent is offline, and `500` for server errors. Updates answer `201`.
-
-### Objects
+| Status | Meaning |
+| --- | --- |
+| `200`, `201`, `202` | Success. `202`: a firmware update was started or queued. |
+| `400` | Invalid input. |
+| `401` | No or an invalid token, or the endpoint needs a login token. |
+| `403` | Not allowed, for example your role on a shared device or a non-admin calling an admin endpoint. |
+| `404` | Not found, or not yours. |
+| `409` | Conflict, for example a duplicate username, or a device without an agent or method for the action. |
+| `429` | Rate limit. |
+| `502` | The agent reported an error. |
+| `503` | The agent is offline. |
+| `504` | The agent didn't answer in time. |
+
+Timestamps are Unix times in milliseconds.
-```json [Agent]
-{
- "id": 3,
- "name": "Office ESP32",
- "type": "microcontroller",
- "description": "Under the desk",
- "ownerID": 1
-}
-```
+## Devices
-| Agent field | Type | Description |
-| --- | --- | --- |
-| `id` | number | Agent ID, also the Connection ID. |
-| `name` | string | Display name. |
-| `type` | `"server"` \| `"microcontroller"` | Agent type. |
-| `description` | string | Free text; may be empty. |
-| `ownerID` | number | ID of the owning user. |
+All endpoints return the devices you own and the devices shared with you; your role on each is in
+`access_role`.
-Agent objects never contain the secret. The server stores only a hash of it and returns the
-secret once, when you [create an agent](#create-an-agent) or
-[regenerate its secret](#regenerate-an-agent-secret).
+| Method and path | Description |
+| --- | --- |
+| `GET /devices` | List devices. |
+| `POST /devices` | Create a device. |
+| `GET /devices/` | Get a device. |
+| `PUT /devices/` | Change a device (owner, manager; agents: owner only). All fields optional. |
+| `DELETE /devices/` | Delete a device (owner). |
+| `POST /devices//power-on` | Send Wake-on-LAN (owner, manager, operator). |
+| `POST /devices//shutdown` | Shut down through the Linux agent on the device or SSH (owner, manager, operator). |
+| `POST /devices//reboot` | Reboot, like shutdown. |
+| `POST /devices//status` | Check the status now and return it (at most every 5 seconds per device). |
+| `PUT /devices//favorite` | Add to your favorites. `DELETE` removes it. |
```json [Device]
{
- "id": 1,
+ "id": 4,
"name": "Gaming PC",
"type": "desktop",
- "description": null,
- "macAddress": "AA:BB:CC:DD:EE:FF",
- "port": 9,
- "agentID": 3,
- "ownerID": 1
+ "description": "",
+ "mac_address": "AA:BB:CC:DD:EE:FF",
+ "wol_port": 9,
+ "agent_id": 3,
+ "status_method": "ipv4",
+ "status_address": "192.168.1.20",
+ "status_tcp_port": 3389,
+ "host_agent_id": null,
+ "created_at": 1792351800000,
+ "access_role": "owner",
+ "owner": { "id": 1, "username": "admin", "display_name": "Admin" },
+ "agent": { "id": 3, "name": "Office ESP32", "kind": "esp32", "state": "online" },
+ "host_agent": null,
+ "favorite": true,
+ "status": { "state": "online", "checked_at": 1792351812000 },
+ "actions": { "power_on": true, "shutdown": false, "reboot": false }
}
```
-| Device field | Type | Description |
-| --- | --- | --- |
-| `id` | number | Device ID. |
-| `name` | string | Display name. |
-| `type` | `"server"` \| `"desktop"` \| `"laptop"` \| `"printer"` \| `"nas"` | Device type. |
-| `description` | string \| null | Free text. |
-| `macAddress` | string | MAC address, `AA:BB:CC:DD:EE:FF` or `AA-BB-CC-DD-EE-FF`. |
-| `port` | number | Wake-on-LAN UDP port, `1`–`65535` (usually `9`). |
-| `agentID` | number | ID of one of your agents; it wakes this device. |
-| `ownerID` | number | ID of the owning user. |
-
-`id` and `ownerID` are set by the server. Leave them out of request bodies.
-
-## Authentication
-
-### Log in
-
-`POST /api/auth/login`
-
-| Body field | Type | Required |
-| --- | --- | --- |
-| `username` | string | yes |
-| `password` | string | yes |
-
-Sets the `session` cookie and answers `200` with `Login successful`. Wrong credentials return
-`401` with `No matching credentials found`.
-
-### Log out
-
-`POST /api/auth/logout`
-
-Ends the session and clears the cookie. Always answers `200`.
-
-### Get the session
-
-`GET /api/auth/session`
-
-Returns the logged-in user, or `401` if there is no valid session.
-
-```json
-{
- "status": "OK",
- "message": "Session valid",
- "data": {
- "userID": 1,
- "username": "admin",
- "role": "admin",
- "favorites": [],
- "expirationTimestamp": 1792351800000
- }
-}
-```
-
-## Account
-
-### Update an account
-
-`PUT /api/account/`
-
-Updates the username and password of the logged-in user.
+| Field | Description |
+| --- | --- |
+| `type` | `server`, `desktop`, `laptop`, `printer`, `nas` or `other`. |
+| `mac_address` | `AA:BB:CC:DD:EE:FF` or `AA-BB-CC-DD-EE-FF` in requests; stored with colons in uppercase. |
+| `wol_port` | `1`–`65535`, default `9`. |
+| `agent_id` | The agent that wakes and checks the device, or `null`. |
+| `status_method` | `none` (default), `link_local`, `ipv4`, `ipv6`, `ssh` or `host_agent`. See [Status checks](/docs/guides/devices#status-checks). |
+| `status_address` | Required for `ipv4` (IPv4 address), `ipv6` (IPv6 address) and `ssh` (IP address or host name). |
+| `status_tcp_port` | Optional extra TCP probe; the port for `ssh` (default `22`). |
+| `host_agent_id` | The Linux agent running on the device, or `null`. |
+| `access_role` | `owner`, `manager`, `operator` or `viewer`. |
+| `status` | `state` (`online`, `offline`, `unknown`), `checked_at` and, for `unknown`, a `reason`. |
+| `actions` | Which power actions are possible right now for you. |
+
+Required when creating: `name`, `type` and `mac_address`. Agents must be your own;
+`host_agent_id` must be a Linux agent.
+
+### Sharing
+
+Owner only, except leaving a device.
+
+| Method and path | Description |
+| --- | --- |
+| `GET /devices//shares` | List the shares: `user`, `role`, `created_at`. |
+| `POST /devices//shares` | Share: `{ "user": "", "role": "viewer" \| "operator" \| "manager" }`. |
+| `PUT /devices//shares/` | Change the role: `{ "role": "manager" }`. |
+| `DELETE /devices//shares/` | Remove a share. With your own user ID, you leave a shared device. |
-| Body field | Type | Required | Description |
-| --- | --- | --- | --- |
-| `username` | string | yes | New or current username. Must be unique (`409` otherwise). |
-| `favorites` | number[] | yes | IDs of favorite devices. Send the current list (`[]` if none). |
-| `password` | string | no | New password. Omit it or send an empty string to keep the current one. |
-| `role` | `"user"` \| `"admin"` | no | See below. |
+### SSH login
-Answers `201` with `User Account updated successfully`.
+Owner only. The password or private key is never returned. See [SSH](/docs/guides/ssh).
-**Admin mode.** Admins can add `?adminMode=true` to update the user given in the path instead of
-themselves. The first user (ID `1`) can also change other users' `role`, and must send `role` when
-editing someone else.
+| Method and path | Description |
+| --- | --- |
+| `GET /devices//ssh` | The login without its secret, with `authorized_keys` and `sudoers` lines. |
+| `PUT /devices//ssh` | Set the login: `host` (or `null` for the status address), `port`, `username`, `os` (`posix`, `windows`), `sudo`, and `auth`: `{ "type": "generate_key" }`, `{ "type": "password", "password": "…" }` or `{ "type": "keep" }`. |
+| `DELETE /devices//ssh` | Remove the login. |
+| `POST /devices//ssh/test` | Let the device's agent log in and report the host key: `online`, `authenticated`, `host_key`, `trusted`, `error`. |
+| `POST /devices//ssh/trust` | Trust a host key: `{ "host_key": "SHA256:…" }`. |
## Agents
-### List agents
-
-`GET /api/agents` returns all of your agents as an array of [agent objects](#objects).
-
-### Create an agent
+Agents are never shared: these endpoints only see your own.
-`POST /api/agents`
-
-| Body field | Type | Required |
-| --- | --- | --- |
-| `name` | string | yes |
-| `type` | `"server"` \| `"microcontroller"` | yes |
-| `description` | string | yes (may be empty) |
-
-Answers `201` with the new agent's ID and its generated secret. Store the secret now: it isn't
-returned again. The agent can connect right away.
+| Method and path | Description |
+| --- | --- |
+| `GET /agents` | List agents. |
+| `POST /agents` | Create an agent: `{ "name": "…", "description": "…", "kind": "esp32" \| "linux" }`. |
+| `GET /agents/` | Get an agent. |
+| `PUT /agents/` | Change `name` and `description`. |
+| `DELETE /agents/` | Delete and disconnect. Its devices keep their settings without an agent. |
+| `POST /agents//enrollments` | Create a single-use enrollment token (15 minutes): `{ "token", "expires_at" }`. |
+| `POST /agents//revoke` | Revoke the credential and disconnect. |
+| `POST /agents//flash-image` | ESP32: the NVS image for the web installer: `{ "version": "…", "server_url"?: "…" }` → `{ "image" (base64), "offset", "server_url", "expires_at" }`. Contains a new enrollment token. |
+| `POST /agents//update` | ESP32: start a firmware update: `{ "version"?: "…" }` (default: the recommended release). |
+| `POST /agents/bulk-update` | ESP32: update several agents one after another: `{ "agent_ids": [3, 5], "version"?: "…" }` → `{ "version", "queued", "skipped" }`. |
+| `GET /agents//logs` | The agent's stored log lines and log level. |
+| `GET /agents//info` | Ask the agent for current diagnostics. |
+| `PUT /agents//device-config` | Change settings on the agent: `server_url`, `name`, `log_level`, `ip`. |
+| `POST /agents//reboot` | Restart the agent. |
+| `POST /agents//factory-reset` | Erase the agent's settings and revoke its credential. |
+| `POST /agents/enroll` | Used by agents, without user authentication. See [Agent Protocol](/docs/agents/protocol#enrollment). |
-```json
+```json [Agent]
{
- "status": "OK",
- "message": "Agent created successfully",
- "data": { "id": 3, "secret": "6f1c2e9a0b7d4c3e8a5f1b2c3d4e5f60" }
+ "id": 3,
+ "name": "Office ESP32",
+ "description": "",
+ "kind": "esp32",
+ "state": "online",
+ "version": "2.0.0",
+ "capabilities": ["wake", "status.link_local", "status.ip", "status.host", "status.tcp", "ota", "logs", "config", "device.reboot", "device.reset", "ssh"],
+ "info": { "name": "Office-ESP32", "ipv4": "192.168.1.50", "rssi_dbm": -58 },
+ "last_seen_at": 1792351812000,
+ "enrolled_at": 1792350000000,
+ "created_at": 1792349000000,
+ "update": null,
+ "update_available": null
}
```
-### Get agent statuses
-
-`GET /api/agents/status?ids=1,2`
-
-Returns the connection status of your agents as a map from agent ID to `"online"`, `"offline"`
-or `"unknown"`. Without `ids`, all of your agents are included.
-
-```json
-{ "status": "OK", "data": { "1": "online", "2": "offline" } }
-```
-
-### Get an agent
-
-`GET /api/agents/` returns one [agent object](#objects), or `404`.
-
-### Update an agent
-
-`PUT /api/agents/` takes the same body as [Create an agent](#create-an-agent) and answers
-`201`, or `404` if the agent doesn't exist. It changes only the name, type and description.
-
-### Regenerate an agent secret
+`state` is `online`, `offline` or `unenrolled` (*Not set up*). `update` is the current or last
+firmware update (`version`, `from_version`, `state`: `queued`, `starting`, `downloading`,
+`restarting`, `done` or `failed`, `percent`, `error`), kept in memory for 24 hours.
+`update_available` is a newer release of the admin's channel, if the agent can be updated.
-`POST /api/agents//regenerate-secret` (no body)
+## Firmware
-Replaces the agent's secret and answers `200` with the new one, in the same shape as
-[Create an agent](#create-an-agent). The old secret stops working immediately and a connected
-agent is disconnected. Returns `404` if the agent doesn't exist.
-
-### Delete an agent
-
-`DELETE /api/agents/` answers `200`, or `404` if the agent doesn't exist. A connected agent is
-disconnected. Devices assigned to the agent are not deleted; reassign them first.
-
-## Devices
-
-### List devices
-
-`GET /api/devices` returns all of your devices as an array of [device objects](#objects). Add
-`?onlyFavorites=true` to return only the devices in your favorites list.
-
-### Create a device
+| Method and path | Description |
+| --- | --- |
+| `GET /firmware` | The synced ESP32 releases: `{ "channel", "recommended", "releases": [...] }`, each with its flash parts, offsets and SHA-256s. |
+| `GET /firmware//files/` | Download a firmware file. ESP32 agents also use it with their agent credential for updates. |
-`POST /api/devices`
+## Account and authentication
-| Body field | Type | Required |
-| --- | --- | --- |
-| `name` | string | yes |
-| `type` | `"server"` \| `"desktop"` \| `"laptop"` \| `"printer"` \| `"nas"` | yes |
-| `description` | string | no |
-| `macAddress` | string (`AA:BB:CC:DD:EE:FF` or `AA-BB-CC-DD-EE-FF`) | yes |
-| `port` | integer (`1`–`65535`) | yes |
-| `agentID` | number | yes |
+| Method and path | Description |
+| --- | --- |
+| `POST /auth/login` | `{ "username", "password" }` → a login token in `data.token`. |
+| `GET /auth/session` | The current login (login token only). |
+| `POST /auth/logout` | End the current login. |
+| `GET /auth/registration` | `{ "enabled": true \| false }`: whether public sign-up is on. |
+| `POST /auth/signup` | Create an account while public sign-up is on: `{ "username", "email", "password", "display_name"? }`. |
+| `POST /auth/reset-password/request` | Send a reset link to `{ "email" }` (needs email to be configured). |
+| `POST /auth/reset-password` | `{ "reset_token", "new_password" }`. |
+| `GET /account`, `PUT /account` | Your profile; changes need `current_password`. |
+| `PUT /account/password` | `{ "current_password", "new_password" }`. Ends all your logins. |
+| `DELETE /account` | Delete your account with its agents and devices. |
+| `GET /account/apikeys`, `POST /account/apikeys` | List or create API keys: `{ "description", "expires_at": "7d" \| "30d" \| "90d" \| "180d" \| "365d" \| null }` → `{ "id", "token" }`. |
+| `DELETE /account/apikeys/` | Delete an API key. |
+
+## Admin
+
+Admins only, with a login token or an admin's API key.
+
+| Method and path | Description |
+| --- | --- |
+| `GET /admin/users`, `POST /admin/users` | List users (`?role=`, `?search=`) or create one: `{ "username", "display_name", "email", "password", "role" }`. |
+| `GET`, `PUT`, `DELETE /admin/users/` | Get, change (`display_name`, `email`, `role`, …) or delete a user. |
+| `PUT /admin/users//password` | Set a user's password and end their logins. |
+| `GET /admin/settings`, `PUT /admin/settings` | `{ "registration_enabled": false, "firmware_channel": "stable" \| "prerelease" }`. |
+| `GET /admin/firmware` | The firmware sync state. |
+| `POST /admin/firmware/sync` | Sync firmware releases from GitHub now. |
-Answers `201`; `data` is the new device's ID. Invalid fields return `400` with `Invalid payload`.
-An `agentID` that isn't one of your agents returns `400` with
-`No matching agent found for the given AgentID`.
+## Other endpoints
-### Get device statuses
+| Path | Description |
+| --- | --- |
+| `GET /api/health` | `200` while the server runs. Used by Docker's health check. |
+| `GET /api/docs/v1` | The OpenAPI reference. `/api` redirects there. |
+| `/ws/agent` | The agent WebSocket. See [Agent Protocol](/docs/agents/protocol). |
+| `/ws/console` | The live log of an agent, for the dashboard. See [below](#the-console-websocket). |
+| `GET /install/linux.sh` | The Linux agent's install script. See [Linux Agent](/docs/agents/linux#install). |
-`GET /api/devices/status?ids=1,2`
+### The console WebSocket
-Asks each device's agent whether the device is online and returns a map from device ID to
-`"online"`, `"offline"` or `"unknown"`. Without `ids`, all of your devices are checked. The
-devices are checked one after another, and each check can take up to 5 seconds, so filter with
-`ids` when you only need a few.
+`/ws/console` streams an agent's log to a browser. The connection must come from a page on
+`NETIGNITE_APP_URL` (other `Origin` headers get `403`). The first message authenticates within 10
+seconds; only the agent's owner may watch it:
```json
-{ "status": "OK", "data": { "1": "online", "4": "unknown" } }
+{ "type": "auth", "token": "", "agent_id": 3 }
```
-### Get a device
-
-`GET /api/devices/` returns one [device object](#objects), or `404`.
-
-### Update a device
-
-`PUT /api/devices/` takes the same body as [Create a device](#create-a-device) and answers
-`201`, or `404` if the device doesn't exist.
-
-### Delete a device
-
-`DELETE /api/devices/` answers `200`, or `404` if the device doesn't exist.
-
-### Wake a device
-
-`POST /api/devices//wakeup` (no body)
-
-Sends a Wake-on-LAN command through the device's agent.
-
-| Status | Message | Meaning |
-| --- | --- | --- |
-| `200` | `Wakeup command sent` | The agent sent the magic packet. |
-| `404` | `Device not found or inaccessible by user` / `Agent not found` | Unknown device, or its agent no longer exists or belongs to another user. |
-| `503` | `Agent is offline` | The agent isn't connected. |
-| `500` | `Failed to send wakeup command` | The agent didn't answer within 5 seconds. |
-| `500` | `Wakeup command failed: …` | The agent rejected the command, for example because of an invalid MAC address. |
-
-## Agent WebSocket
-
-`/api/agent-control-service` is the WebSocket endpoint for agents, not a REST endpoint. See
-[Agent Protocol](/docs/agents/protocol).
+The server then sends `{ "type": "ready", "online": true }`, the stored lines as
+`{ "type": "lines", "lines": [...], "reset": true, "level": "info" }`, new lines as
+`{ "type": "lines", "lines": [...], "reset": false }`, `{ "type": "state", "online": false }` when the
+agent disconnects, and `{ "type": "error", "message": "…" }` before closing (close codes `4401`
+not authenticated, `4404` agent not found, `1008` invalid message). The token is checked again
+every minute.
diff --git a/content/docs/reference/development.md b/content/docs/reference/development.md
index d6efd97..b82d64a 100644
--- a/content/docs/reference/development.md
+++ b/content/docs/reference/development.md
@@ -1,6 +1,6 @@
---
title: "Development"
-description: "NetIgnite's repositories, how to run each one locally, and how to contribute."
+description: "NetIgnite's repositories and branches, how to run each one locally, the tests, and how to contribute."
navigation:
title: Development
---
@@ -13,45 +13,98 @@ licensed under the [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html).
| Repository | Stack |
| --- | --- |
-| [Server](https://github.com/NetIgnite-project/Server) | Nuxt 3, Nitro (Bun preset) with WebSockets, SQLite via libsql, Bootstrap |
-| [Agent-ESP32](https://github.com/NetIgnite-project/Agent-ESP32) | C++17, Arduino framework, PlatformIO |
+| [Server](https://github.com/NetIgnite-project/Server) | Nuxt 4, Nuxt UI, Hono with Zod and OpenAPI, Drizzle ORM on `bun:sqlite`, Bun |
+| [Agent-ESP32](https://github.com/NetIgnite-project/Agent-ESP32) | C++17, Arduino-ESP32 3.3 on ESP-IDF 5.5 (pioarduino), PlatformIO |
+| [Agent-Linux](https://github.com/NetIgnite-project/Agent-Linux) | TypeScript, Bun, compiled into single binaries |
| [Website](https://github.com/NetIgnite-project/Website) | Nuxt 4, Nuxt UI, Nuxt Content, Tailwind CSS, static generation |
+## Branches and releases
+
+`main` holds the released version of each repository. Until NetIgnite v2 has its stable release,
+v2 is developed on the `v2` branches of Server and Agent-ESP32, and their `main` branches hold v1.
+
+| Repository | Release |
+| --- | --- |
+| Server | The manual GitHub Actions workflow **Create Release** builds, tests, creates the GitHub release and publishes the Docker image. Stable releases (which move the image's `latest` tag) only run from `main`; pre-releases such as `2.0.0-beta.1` from any branch. |
+| Agent-ESP32 | Pushing a tag `v` builds, tests and publishes a release with the flash images and `release.json`. Versions with a suffix become pre-releases. |
+| Agent-Linux | Pushing a tag `v` that matches `package.json` builds the three binaries and `SHA256SUMS`. Versions with a suffix become pre-releases. |
+| Website | Every push to `main` is deployed to netignite.dev. Docs for unreleased versions live on a separate branch until the release. |
+
+The agents and the server talk over the [agent protocol v2](/docs/agents/protocol); its source of
+truth is the server (`server/lib/agents/protocol.ts`). Change the ESP32's NVS keys
+(`src/Config.h`) only together with the server's web installer
+(`server/lib/firmware/provisioning.ts`).
+
## Server
```bash [Terminal]
git clone https://github.com/NetIgnite-project/Server.git
cd Server
bun install
+cp example.env .env
bun run dev
```
-The dev server runs on `http://localhost:3000` and stores its database in `./data/`. The admin
-password is printed on first start. Run the tests with `bun test`.
+The dev server runs on `http://localhost:18232` with the dashboard, the API, the agent gateway and
+the OpenAPI reference at `/api/docs/v1`. It stores its data in `./data/` and `./config/`; on the
+first start it prints the link for the `admin` password. `example.env` contains sample SMTP
+settings; remove them if you don't have a mail server.
+
+| Command | What it does |
+| --- | --- |
+| `bun test` | Runs the tests: API routes and permissions, the agent gateway with a simulated agent, the v1 import, the firmware catalog against a fake GitHub, the NVS image against Espressif's generator, OTA, console and SSH. |
+| `bun run check` | Biome lint and format check. |
+| `bun run typecheck` | `nuxt typecheck` and `tsc` for `server/`. |
+| `bun run api-client:generate` | Regenerates the dashboard's typed API client after API changes. |
+| `bun run db:generate` | Creates a Drizzle migration after schema changes. |
+| `bun run build` | Production build in `.output/`, including `.output/import-v1.mjs`. |
-Helper scripts in `devutils/` write directly to `./data/db.sqlite`:
+To test agents without hardware, run the [Linux agent](#linux-agent) or the
+[example agent](/docs/agents/protocol#example-agent) against the dev server.
-- `bun run devutils/createAgent.ts` creates a test agent (secret `123456`) for user `1`. Restart
- the dev server afterwards so it loads the agent.
-- `bun run devutils/createTestUser.ts` creates an `admin` user with the password `123456`. It
- fails if `admin` already exists, so run it before the server's first start.
+## ESP32 agent
-Releases are created with the manual GitHub Actions workflows **Create Release** (builds, tests
-and attaches `app.tar.gz`) and **Publish Docker Images** (pushes to
-`ghcr.io/netignite-project/server`).
+You need [PlatformIO](https://platformio.org/) Core 6.2 or newer. Building, flashing and the serial
+console are described on the [ESP32 Agent](/docs/agents/esp32#build-from-source) page. A board
+flashed from PlatformIO is set up over the serial console:
+`setup http://:18232 `.
-## ESP32 Agent
+| Command | What it does |
+| --- | --- |
+| `pio run -e esp32dev` | Builds the firmware. |
+| `pio test -e native` | Unit tests of the parsers and encoders on your computer (needs `gcc`). |
+| `python scripts/check_size.py` | Fails if the firmware takes more than 85 % of a firmware slot. |
+| `pio run -e esp32dev-qemu` | The firmware without Wi-Fi, for Espressif's QEMU. |
-Open the repository in VS Code with the PlatformIO extension, or use PlatformIO Core. Setup,
-build and flashing are described on the [ESP32 Agent](/docs/agents/esp32) page. The code is
-formatted with the included `.clang-format`.
+`test/emulator/` boots the QEMU build and tests Improv, the console commands and OTA updates
+(signature checks, probation, rollback) with Bun; its README has the commands. QEMU has no Wi-Fi,
+so networking needs a real board. The code is formatted with the included `.clang-format`.
+
+## Linux agent
+
+```bash [Terminal]
+git clone https://github.com/NetIgnite-project/Agent-Linux.git
+cd Agent-Linux
+bun install
+bun run dev version
+```
+
+| Command | What it does |
+| --- | --- |
+| `bun run dev ` | Runs the agent from source with file watching, for example `bun run dev run --config ./agent.json`. |
+| `bun test` | Unit and integration tests against a fake server and an SSH server. |
+| `bun run typecheck` | TypeScript check. |
+| `bun run check` | Biome. |
+| `bun run compile all --no-version-tag` | Builds `build/bin/netignite-agent-linux-{x64,x64-baseline,arm64}`. |
-To test protocol changes without hardware, use the [example agent](/docs/agents/protocol#example-agent).
+Pair a development agent with a config outside `/etc`:
+`bun run dev pair http://localhost:18232 --config ./agent.json`. `install` only works from
+a compiled binary.
## Website
This website, including the docs, is a static site built with the
-[LeiCraftMC Style Guide](https://git.leicraftmc.de/LeiCraftMC/Style-Guides) static-site-with-docs
+[LeiCraftMC Style Guide](https://github.com/LeiCraftMC/Style-Guides) static-site-with-docs
template.
```bash [Terminal]
@@ -62,7 +115,7 @@ bun run dev
```
The dev server runs on `http://localhost:12531`. `bun run generate` writes the static site to
-`.output/public/`.
+`.output/public/`; `bun test` and `bun run check` run the tests and Biome.
Docs pages are Markdown files in `content/docs/`. To add one:
@@ -70,10 +123,12 @@ Docs pages are Markdown files in `content/docs/`. To add one:
2. Register it in `app/data/docs.ts` so it appears in the sidebar and the previous/next links.
3. Add it to `public/static/utils/sitemap.xml`.
-Every page has an **Edit this page** link that opens the file on GitHub.
+Label code blocks with a file name or an extension from `app/data/code-icons.ts`, or a label without
+a dot such as `[Terminal]`; a test checks this. Every page has an **Edit this page** link that
+opens the file on GitHub.
## Contributing
Bug reports and pull requests are welcome in the matching repository. Use
[Conventional Commits](https://www.conventionalcommits.org/) for commit messages, for example
-`fix: reconnect after WiFi loss`.
+`fix: reconnect after Wi-Fi loss`.
diff --git a/content/docs/reference/troubleshooting.md b/content/docs/reference/troubleshooting.md
index 6c82fb5..404c667 100644
--- a/content/docs/reference/troubleshooting.md
+++ b/content/docs/reference/troubleshooting.md
@@ -1,122 +1,243 @@
---
title: "Troubleshooting"
-description: "Solutions for common NetIgnite problems: logins, agents that don't connect, devices that don't wake, and wrong status."
+description: "Solutions for common NetIgnite problems: the server doesn't start, logins, the web installer, agents that don't connect, devices that don't wake, wrong status, shutdown, updates and the v1 import."
navigation:
title: Troubleshooting
---
# Troubleshooting
+Start with the logs: the server's (`docker logs netignite-server`), the ESP32's
+[serial console](/docs/agents/esp32#serial-console) or **Console** tab, and the Linux agent's
+journal (`journalctl -u netignite-agent`).
+
+## Server
+
+### The server doesn't start
+
+| Log message | Fix |
+| --- | --- |
+| `Failed to read the environment variable APP_URL: …` | Set `NETIGNITE_APP_URL`; it's required. |
+| `Failed to read the environment variable : Expected a boolean value ('true' or 'false')` | Booleans must be exactly `true` or `false`, not empty, `1` or `yes`. |
+| `NETIGNITE_FIRMWARE_PUBLIC_KEY: can't use …` | `NETIGNITE_FIRMWARE_PUBLIC_KEY` is a *path* to a PEM public key; the file must exist inside the container, for example in the config volume. Until it can be read, every firmware sync fails with this message (also under **Admin → Server Settings**). |
+
+### The firmware list stays empty
+
+The web installer says *No firmware available yet*. Under **Admin → Server Settings**, click
+**Sync now** and read the result:
+
+- *GitHub answered 403 …* or another HTTP error: the server can't reach GitHub or hit GitHub's rate
+ limit. Allow outbound HTTPS, or set `NETIGNITE_GITHUB_TOKEN`.
+- *Skipped … the release isn't signed, but FIRMWARE_PUBLIC_KEY requires a signature*: with
+ `NETIGNITE_FIRMWARE_PUBLIC_KEY` set, unsigned releases are skipped. Remove the variable or use
+ signed releases.
+- *Skipped … the firmware speaks agent protocol …*: the release is for another server version.
+
+Servers without internet access can't sync firmware. Build the firmware yourself and set the board
+up over the [serial console](/docs/agents/esp32#build-from-source).
+
## Logging in
-### The login form returns to the login page
+### The login page comes back after logging in
-Production builds only accept the session cookie over HTTPS (or on `localhost`). If you open the
-dashboard as `http://:18232`, the browser drops the cookie and every page redirects
-back to the login. Put the server behind [HTTPS](/docs/server/reverse-proxy), or open
-`http://localhost:18232` on the server itself (for example through an SSH tunnel:
-`ssh -L 18232:localhost:18232 `).
+- **No HTTPS.** The browser drops the login cookie on plain `http://` pages other than `localhost`.
+ Put the server behind [HTTPS](/docs/server/reverse-proxy), or open `http://localhost:18232` on
+ the server itself (for example through an SSH tunnel: `ssh -L 18232:localhost:18232 `).
+- **Wrong `NETIGNITE_APP_URL`.** The dashboard sends its API requests to `NETIGNITE_APP_URL`. If it
+ isn't the address in the browser's address bar, logins and everything else fail. Fix it and
+ restart the server.
-### Everyone was logged out
+### "Too many login attempts"
-Sessions are kept in memory, so every server restart or update ends all sessions. Log in again.
+After 5 failed logins for a username from one address (15 from all addresses), the server refuses
+logins for that username until 5 minutes after the first attempt. Behind a reverse proxy, all
+clients share the proxy's address. Wait, then try again.
+
+### The reset link email doesn't arrive
+
+Password-reset emails need [email](/docs/server/configuration#email) to be configured, and they go
+to the account's email address. Imported v1 users and the first admin have placeholder addresses
+until they change them. An admin can set a new password under **Admin → Users → Reset Password**.
### I lost the admin password
-If `data/initial_admin_credentials` still exists and you never changed the password, it's in
-there. Otherwise, set a new password directly in the database. With Docker, replace
-`NEW-PASSWORD` and run:
+If another admin exists, they can reset it under **Admin → Users**. If the first admin's link in
+`config/initial_admin_password_reset_token.txt` was never used and is less than 7 days old, use it.
+Otherwise, set a new password directly in the database. With Docker, replace `NEW-PASSWORD` and run:
```bash [Terminal]
docker exec netignite-server bun -e '
const { Database } = require("bun:sqlite");
- const hash = await Bun.password.hash("NEW-PASSWORD", { algorithm: "bcrypt", cost: 12 });
- new Database("data/db.sqlite").run("UPDATE users SET password_hash = ? WHERE username = ?", [hash, "admin"]);
+ const hash = await Bun.password.hash("NEW-PASSWORD");
+ new Database("/opt/netignite/data/netignite.sqlite").run("UPDATE users SET password_hash = ? WHERE username = ?", [hash, "admin"]);
'
```
+## Web installer
+
+| Problem | Fix |
+| --- | --- |
+| *This browser can't access USB devices* | Use Google Chrome, Microsoft Edge or Firefox 151 or newer on a computer, or set the board up over its [serial console](/docs/agents/esp32#serial-console). |
+| *Installing needs HTTPS* | Open the dashboard over HTTPS (or on `localhost`). |
+| The ESP32's port isn't in the list | Install the USB driver for the board's USB-to-serial chip (often CP210x or CH340), use a data cable, and close other programs that use the port, such as a serial monitor. |
+| *The setup token expired* | The token is valid for 15 minutes. Click **Prepare again**. |
+| Wi-Fi setup fails in the dialog | The ESP32 only supports 2.4 GHz networks. Check the password; a new network is only saved if the connection works. |
+| Installed, but the agent doesn't come online | See [below](#the-agent-doesnt-come-online). |
+
## Agents
### The agent doesn't come online
-Check the server log (`docker logs netignite-server`) and, for the ESP32, the serial monitor.
+**ESP32**: check the serial console or the web installer's **Logs & console**:
-| Symptom | Cause and fix |
+| Message | Cause and fix |
+| --- | --- |
+| `Waiting for Wi-Fi settings` | No Wi-Fi set up. Use **Change Wi-Fi** in the web installer or the `wifi` command. |
+| `Connecting to Wi-Fi "…"` every 30 seconds, `Wi-Fi connection lost` | Wrong network or password, a 5 GHz-only network, or poor signal. Change the Wi-Fi with the web installer or the `wifi` command. |
+| `Couldn't connect to "…"` after `wifi` or **Change Wi-Fi** | The new network didn't work; the previous one is kept. |
+| `Not connected to a server yet` | No server set. Use the web installer, or `setup `. |
+| `Waiting for the time` | The board can't reach NTP (UDP port 123) or DNS. HTTPS connections wait until the time is synced. |
+| `Enrollment rejected: …` | The setup token was used, expired or is for a Linux agent. Create a new one and install again, or use `enroll `. |
+| `Enrollment failed: …` | The server isn't reachable at the configured address. The agent retries every 30 seconds. |
+| `This agent has no credential` | The credential was erased. Set the agent up again. |
+| `Couldn't connect to the server, retrying in … s` | Wrong server address, the certificate isn't trusted (private CA, see [Certificates](/docs/agents/esp32#certificates)), the proxy doesn't forward `/ws/agent`, or the credential was revoked: then set the agent up again. |
+
+**Linux agent**: run `sudo netignite-agent status`. It checks the server (`Server check`) and the
+credential (`Credential`) unless the service is running.
+
+| Message | Cause and fix |
| --- | --- |
-| ESP32 logs `Missing credentials!` | `C_CONTROL_SERVER_AUTH_ID` or `C_CONTROL_SERVER_AUTH_SECRET` is empty. |
-| ESP32 logs `Websocket Connected` and right after `Websocket Disconnected!` | The server rejected the credentials. Check that the ID is the numeric **Connection ID** and that the secret is exactly the one shown when the agent was created. If you're unsure, [regenerate the secret](/docs/guides/agents#changing-the-secret) and reflash. |
-| ESP32 only logs `Websocket Disconnected! Retrying in 60 seconds...` | The connection fails before the handshake: wrong host or port, `USE_SSL` doesn't match the port (`true` for `443`, `false` for a plain `18232`), or the server certificate isn't trusted (see [Certificate bundle](/docs/agents/esp32#certificate-bundle)). |
-| ESP32 logs `NTP time sync timed out` | The board has no internet access, DNS doesn't work, or NTP (UDP port 123) is blocked. TLS connections fail until the time is synced; the agent keeps retrying on its own once the network works. |
-| ESP32 logs `WiFi lost connection` repeatedly | Wrong SSID or password, or the network is 5 GHz only. |
-| The server logs `Agent '' connected`, but the dashboard shows offline | The status refreshes every 20 seconds; use the **Ping** button to refresh it. |
+| `The server doesn't accept this agent's credential (HTTP 401)`, exit code `78` | The credential was revoked or the agent deleted. Pair again with a new token: `sudo netignite-agent pair `. |
+| `The server revoked this agent's credential` | Same: it was paired again elsewhere, revoked or reset in the dashboard. |
+| `Another connection with this agent's credential replaced this one` | The agent runs twice, for example `run` by hand next to the service. |
+| `The server didn't accept the HELLO` | The agent and the server don't speak the same protocol version. Update the agent or the server. |
+| `No config at /etc/netignite/agent.json` | Pair the agent first. |
-### The agent disconnects about once a minute
+The server logs `Agent online (…)` when an agent connects and `Agent disconnected` when
+it goes.
-A reverse proxy is closing idle WebSocket connections. Raise its idle/read timeout above 60
-seconds, as shown in [HTTPS & Reverse Proxy](/docs/server/reverse-proxy).
+### The agent disconnects regularly
-### The ESP32 firmware doesn't build
+- A reverse proxy closes idle WebSockets. Raise its timeout above 60 seconds; see
+ [HTTPS & Reverse Proxy](/docs/server/reverse-proxy).
+- `Agent unresponsive, closing` in the server log: the agent didn't send anything for 75
+ seconds. For an ESP32, check its Wi-Fi signal (`rssi_dbm` on the agent's page).
-- **The certificate bundle can't be generated:** the first build installs the Python package
- `cryptography` into PlatformIO's environment and needs internet access for that. See
- [Certificate bundle](/docs/agents/esp32#certificate-bundle).
-- **`config/config.h` not found:** copy `src/config/config.sample.h` to `src/config/config.h`.
-- **Errors about `IPv6Address`, `enableIpV6` or `beginSslWithBundle`:** you're building with
- Arduino-ESP32 core 3.x. Use PlatformIO's official `espressif32` platform, which ships core 2.x.
+### The Console tab says "Agent offline" or "Disconnected"
+
+*Agent offline*: the agent isn't connected; the log appears when it is. *Disconnected*: the
+browser's connection to `/ws/console` failed. Check that the proxy forwards WebSockets on
+`/ws/console` and that you opened the dashboard at `NETIGNITE_APP_URL` (the server refuses other
+origins).
## Waking devices
+### The power button is disabled
+
+Its tooltip says *No online agent with Wake-on-LAN*: the device has no agent, its agent is offline,
+or the agent can't send Wake-on-LAN packets (a Linux agent without
+[Wake-on-LAN mode](/docs/agents/linux#wake-on-lan-mode)). Viewers of a shared device don't get the
+button at all.
+
### The device doesn't wake up
-*Wakeup command sent* only means the agent sent the packet. If the device stays off:
+*Wake-on-LAN packet sent* only means the agent sent the packet. If the device stays off:
1. **Wake-on-LAN enabled?** Check the BIOS/UEFI and the network driver settings, and disable
ErP/EuP power saving. See [Preparing a device](/docs/guides/devices#preparing-a-device).
2. **Right MAC address?** Use the MAC of the **wired** network card the device is connected with,
- not its WiFi adapter.
+ not its Wi-Fi adapter.
3. **Same network?** The agent must be in the same subnet as the device. Magic packets are
broadcasts and don't cross routers or VLANs.
4. **Right port?** Most devices listen on `9`; some only react to `7`.
5. **Power state.** Some devices only wake from sleep or hibernation, not from a full shutdown.
On Windows, disabling Fast Startup often helps.
-6. **Test locally.** Send a magic packet from another computer in the same network (for example
- with `wakeonlan` or `etherwake`). If that doesn't work either, the problem is the device, not
- NetIgnite.
+6. **Test locally.** Send a magic packet from another machine in the same network, for example with
+ `netignite-agent wol AA:BB:CC:DD:EE:FF` on a Linux agent, `wakeonlan` or `etherwake`. If that
+ doesn't work either, the problem is the device, not NetIgnite.
+
+### Error messages
-### "Agent is offline"
+| Message | Meaning |
+| --- | --- |
+| *No agent is assigned to this device* | Choose an agent in the device's settings. |
+| *Agent is offline* | See [The agent doesn't come online](#the-agent-doesnt-come-online). |
+| *The agent didn't answer in time* | The agent didn't answer within 5 seconds, often because its connection had just dropped. Try again. |
+| *The agent doesn't support this action* | The agent can't do that, for example Wake-on-LAN on a Linux agent without Wake-on-LAN mode. |
+| *Your role on this device doesn't allow power actions* | You're a viewer of this shared device. |
-The device's agent isn't connected. See [The agent doesn't come online](#the-agent-doesnt-come-online).
+## Device status
-### "Wakeup command failed: Wakeup failed: Invalid MAC address or port"
+### A running device shows as offline
-The ESP32 agent only accepts MAC addresses like `AA:BB:CC:DD:EE:FF` or `AA-BB-CC-DD-EE-FF` and
-ports from `1` to `65535`. Edit the device and fix the value.
+- **MAC (IPv6 link-local)** only works when the device uses its MAC-based IPv6 link-local address
+ and answers IPv6 pings. Windows and many Linux distributions use random addresses. Switch to the
+ **IPv4 address** check, or change the device:
+ - Windows, in an elevated prompt: `netsh interface ipv6 set global randomizeidentifiers=disabled`.
+ - NetworkManager: `nmcli connection modify ipv6.addr-gen-mode eui64`, then reconnect.
+ - systemd-networkd: `IPv6LinkLocalAddressGenerationMode=eui64` in the `[Network]` section.
+- **The device doesn't answer pings.** Windows blocks them by default. Add a **TCP port** the device
+ answers on (`445`, `3389`), or allow *File and Printer Sharing (Echo Request)* in the firewall.
+- **The address changed.** Give the device a fixed address (DHCP reservation).
+- **The agent can't reach the address.** The agent checks the device, not the server: the address
+ must be reachable from the agent's network.
-### "Failed to send wakeup command"
+### The status is "Unknown"
-The agent didn't answer within 5 seconds, often because it was busy with status checks or its
-connection had just dropped. Try again. If it keeps happening, check the agent's connection.
+Hover the status badge for the reason:
-## Device status
+| Reason | Fix |
+| --- | --- |
+| *Status checks are turned off* | Choose a status check in the device's settings. |
+| *No agent assigned* | Choose an agent. |
+| *No status address configured* | Enter the address for the IPv4, IPv6 or SSH check. |
+| *Agent is offline* | Check the agent. |
+| *Agent doesn't support …* | The agent lacks the capability, for example a Linux agent without Wake-on-LAN mode. |
+| *No host agent assigned* | Choose the Linux agent on the device, or another status check. |
+| *Agent didn't answer in time* | The check took longer than 6 seconds. |
-### A running device shows as offline
+## Shutdown and reboot
-The ESP32 agent pings the device's IPv6 link-local address that it derives from the MAC address
-(EUI-64). The device shows as `offline` if it doesn't use that address or doesn't answer pings:
+### Shut down and Reboot are missing
-- **Windows** uses random interface identifiers. Switch to MAC-based ones in an elevated prompt with
- `netsh interface ipv6 set global randomizeidentifiers=disabled`, and allow the firewall rule
- *File and Printer Sharing (Echo Request - ICMPv6-In)*.
-- **Linux with NetworkManager:** `nmcli connection modify ipv6.addr-gen-mode eui64`,
- then reconnect.
-- **Linux with systemd-networkd:** set `IPv6LinkLocalAddressGenerationMode=eui64` in the
- `[Network]` section.
-- **IPv6 disabled** on the device or a firewall that blocks ICMPv6 echo requests also causes
- `offline`.
+The device needs the [Linux agent on the device](/docs/agents/linux#host-mode) (online), or an
+[SSH login](/docs/guides/ssh) with a trusted host key and an online agent that can log in over SSH.
+Viewers don't get power actions.
-Waking the device works regardless of the status.
+### Shutdown requested, but nothing happens
-### Every status is "unknown"
+- **Linux agent on the device:** the dashboard shows why the agent refused (from systemd-logind).
+ Usually polkit is missing (`apt install polkitd`, then `sudo netignite-agent install` again) or a
+ block inhibitor, such as a running package upgrade, holds the shutdown (`systemd-inhibit --list`).
+ If the request succeeded but nothing happened, check the journal for `Host control (shutdown) failed`.
+- **SSH:** errors come back in the toast:
+
+| Message | Fix |
+| --- | --- |
+| *Test the SSH login and trust the device's host key first* | Open the **SSH** tab, **Test login** and **Trust this host key**. |
+| `sudo: a password is required` | The sudoers line is missing. See [SSH](/docs/guides/ssh#linux-macos-bsd). |
+| `host_key_mismatch` | The device's host key changed. Test again and trust the new key if you expected the change. |
+| `auth_failed` | Wrong username, password or `authorized_keys` line. |
+| *The device's agent can't log in over SSH* | The device's agent doesn't support SSH, for example a Linux agent without Wake-on-LAN mode. |
+
+## Firmware updates
+
+| Message | Meaning |
+| --- | --- |
+| *This firmware can't be updated over the network* | The installed firmware has no update support. Install a current one with the web installer once. |
+| *The current firmware isn't confirmed yet; try again shortly* | The agent just updated and is on probation until it reaches the server. |
+| *The download doesn't match its SHA-256* | The download was damaged. Try again. |
+| *This firmware only installs signed updates, and the update has no signature* | The agent requires signed updates; install a signed release. |
+| *The update's signature is invalid* | The release was signed with another key than the one built into the agent. |
+| *The update to … was rolled back* | The new firmware didn't reach the server within 5 minutes after restarting, so the agent went back. Check the server address and the certificate (a firmware with a private CA updated to an official release can't connect). |
+
+## Upgrading from v1
+
+| Message | Fix |
+| --- | --- |
+| `v1 database not found: …` | Check the path given with `--from`; inside the container it's `/opt/netignite/data/db.sqlite`. |
+| `Not a NetIgnite v1 database: table "…" is missing` | `--from` points at another file, for example the v2 `netignite.sqlite`. |
+| `skipped user …: username already exists in v2` | Add `--merge-users`, or rename the v2 user first. |
+| `skipped device …: invalid MAC address` | Add the device again in the dashboard. |
-The devices' agent is offline, or it didn't answer within 5 seconds. Check the agent's status on
-the **Agents** page.
+See [Upgrading from v1](/docs/server/upgrading).
diff --git a/content/docs/server/configuration.md b/content/docs/server/configuration.md
index b5b146c..59033ef 100644
--- a/content/docs/server/configuration.md
+++ b/content/docs/server/configuration.md
@@ -1,75 +1,172 @@
---
title: "Configuration"
-description: "Configure the NetIgnite server: config.json, environment variables, data paths and log levels."
+description: "Configure the NetIgnite server with NETIGNITE_* environment variables: public URL, data paths, secrets, firmware sync, the Linux agent installer and email."
navigation:
title: Configuration
---
# Configuration
-The NetIgnite server is configured with a JSON file and a few environment variables. Agents and
-devices are not part of the configuration. They live in the database and are managed in the
-dashboard.
+The NetIgnite server is configured with environment variables prefixed `NETIGNITE_`. They're read
+once at startup; restart the server after changing them. Agents, devices, users and the settings
+under **Server Settings** live in the database and are managed in the dashboard.
+
+```yaml [docker-compose.yml]
+services:
+ netignite-server:
+ image: ghcr.io/netignite-project/server:latest
+ environment:
+ NETIGNITE_APP_URL: https://netignite.example.com
+ NETIGNITE_LOG_LEVEL: info
+```
-## config.json
+Running from source, put the variables into a `.env` file in the working directory; Bun loads it on
+start. The repository's `example.env` lists all of them.
-The server reads `config/config.json` relative to its working directory (in Docker:
-`/opt/netignite/config/config.json`). If the file is missing, the server copies the sample config
-there on startup. The sample is an empty object, so all settings use their defaults.
+::note
+Booleans must be exactly `true` or `false`. An invalid value, or a missing `NETIGNITE_APP_URL`,
+stops the server at startup with `Failed to read the environment variable `.
+::
-```json [config.json]
-{
- "logLevel": "info"
-}
-```
+## Variables
+
+The *Default* column is what the server uses when the variable isn't set. The Docker image sets some
+of them to paths inside the container (*Docker image* column).
+
+### General
+
+| Variable | Default | Docker image | Description |
+| --- | --- | --- | --- |
+| `NETIGNITE_APP_URL` | *required* | | The public URL of the dashboard, for example `https://netignite.example.com`, without a path. See [below](#the-public-url). |
+| `NETIGNITE_LOG_LEVEL` | `info` | | `debug`, `info`, `warn`, `error` or `critical`. |
+| `NETIGNITE_API_DISABLE_DOCS` | `false` | | `true` turns off the OpenAPI reference at `/api/docs/v1` and its spec. |
+| `NETIGNITE_SECRETS_KEY` | *generated* | | Key that encrypts stored SSH passwords and private keys. Without it, the server generates one in `/secrets.key`. Any string; use a long random one, for example from `openssl rand -hex 32`. Changing it later makes stored SSH logins unreadable. |
+
+### Paths and database
+
+| Variable | Default | Docker image | Description |
+| --- | --- | --- | --- |
+| `NETIGNITE_DB_PATH` | `./data/netignite.sqlite` | `/opt/netignite/data/netignite.sqlite` | The SQLite database. |
+| `NETIGNITE_DB_AUTO_MIGRATE` | `true` | `true` | Apply database migrations at startup. |
+| `NETIGNITE_DB_MIGRATION_DIR` | `./drizzle/migrations` | `/opt/netignite/app/drizzle/migrations` | Where the migrations are. |
+| `NETIGNITE_CONFIG_BASE_DIR` | `./config` | `/opt/netignite/config` | Directory for `secrets.key` and `initial_admin_password_reset_token.txt`. |
+| `NETIGNITE_LOG_DIR` | `./data/logs` | `/opt/netignite/data/logs` | Log files of background tasks. The server's own log goes to stdout. |
+
+### ESP32 firmware
+
+| Variable | Default | Docker image | Description |
+| --- | --- | --- | --- |
+| `NETIGNITE_FIRMWARE_SYNC` | `true` | | Download new firmware releases from GitHub at startup and every 6 hours. `false` for servers without internet access; admins can still click **Sync now**. |
+| `NETIGNITE_FIRMWARE_REPO` | `NetIgnite-project/Agent-ESP32` | | The GitHub repository (`owner/name`) whose releases are synced, for example a fork. |
+| `NETIGNITE_FIRMWARE_DIR` | `./data/firmware` | `/opt/netignite/data/firmware` | Where synced releases are stored. |
+| `NETIGNITE_FIRMWARE_PUBLIC_KEY` | — | | Path to a PEM file with an ECDSA P-256 public key. When set, only releases with a valid `firmware.sig` are synced. If the file can't be read, every sync fails with the reason (shown in Server Settings) and no firmware is accepted. |
+| `NETIGNITE_GITHUB_TOKEN` | — | | A GitHub token for the release sync; raises GitHub's API rate limit. |
+
+### Linux agent installer
+
+| Variable | Default | Docker image | Description |
+| --- | --- | --- | --- |
+| `NETIGNITE_LINUX_AGENT_REPO` | `NetIgnite-project/Agent-Linux` | | GitHub repository that `/install/linux.sh` downloads the agent from. |
+| `NETIGNITE_LINUX_AGENT_VERSION` | — | | A release tag such as `v1.0.0`. Without it, the script installs the newest stable release, or the newest pre-release while there is no stable one. |
-| Key | Type | Default | Description |
+### Email
+
+Email is used for password-reset links (**Forgot password** on the login page). Without
+`NETIGNITE_SMTP_HOST`, email is off; admins then reset passwords under **Admin → Users**.
+
+| Variable | Default | Docker image | Description |
| --- | --- | --- | --- |
-| `logLevel` | `"debug"` \| `"info"` \| `"warn"` \| `"error"` \| `"critical"` | `"info"` | Minimum level of log messages written to the console. |
+| `NETIGNITE_SMTP_HOST` | — | | SMTP server. Setting it turns email on. |
+| `NETIGNITE_SMTP_PORT` | `587` | | SMTP port. |
+| `NETIGNITE_SMTP_SECURE` | `false` | | `true` for implicit TLS (usually port `465`). |
+| `NETIGNITE_SMTP_USERNAME` | — | | SMTP user; without it the server doesn't log in. |
+| `NETIGNITE_SMTP_PASSWORD` | — | | SMTP password. |
+| `NETIGNITE_SMTP_FROM` | `"NetIgnite" ` | | Sender address. |
+
+### Runtime
+
+These aren't NetIgnite settings but are read by the runtime:
+
+| Variable | Default | Docker image | Description |
+| --- | --- | --- | --- |
+| `NITRO_PORT` (or `PORT`) | `3000` | `18232` | HTTP port. `bun run start` sets `PORT=18232`. |
+| `NITRO_HOST` (or `HOST`) | all interfaces | `::` | Address the server binds to. |
+| `NUXT_PUBLIC_APP_URL` | `NETIGNITE_APP_URL` | | Overrides the URL the dashboard in the browser uses. You normally don't need it. |
+
+To use another port with Docker, change the published port instead, for example `"8080:18232"`.
+
+## The public URL
+
+`NETIGNITE_APP_URL` must be the address users type into the browser, because:
-The file is read once at startup; restart the server after changing it. Invalid JSON or an unknown
-log level stops the server.
+- the dashboard sends its API requests to `/api/v1`,
+- the API only allows cross-origin requests from it, and the agent console (`/ws/console`) only
+ accepts connections from a page on it,
+- password-reset links and the first admin's link point to it,
+- the ESP32 web installer and the Linux agent's setup command use it as the server address for
+ agents (you can change the address the ESP32 uses in the installer).
-## Environment variables
+If users open NetIgnite under another address, the dashboard can't reach its API. Use one public
+host name, at the root of the host (no sub-path), with HTTPS.
-| Variable | Default | Description |
+## Server settings in the dashboard
+
+Admins find these under **Admin → Server Settings**. They're stored in the database.
+
+| Setting | Default | Description |
| --- | --- | --- |
-| `NITRO_PORT` | `3000` (`18232` in Docker) | Port of the dashboard, API and agent WebSocket. `PORT` works as well. |
-| `NITRO_HOST` | all interfaces (`0.0.0.0` in Docker) | Address the server binds to. `HOST` works as well. |
-| `CONFIG_FILE_PATH` | `./config/config.json` | Location of the config file. |
-| `NETIGNITE_SAMPLE_CONFIG_PATH` | `./config/config.sample.json` (`/etc/netignite/config.sample.json` in Docker) | File copied to `CONFIG_FILE_PATH` when no config exists. |
+| **Public sign-up** | off | Anyone who can open the server can create an account at `/auth/signup`. See [Users & Accounts](/docs/server/users#public-sign-up). |
+| **Default channel** | Stable releases | Which ESP32 firmware release is recommended in the installer and offered as an update: *Stable releases* or *Include pre-releases*. Users can still pick any synced release. |
+
+The same page shows the firmware sync state, the synced releases and the **Sync now** button.
+
+## Firmware sync
-To change the port of the Docker image, change the published port instead:
-`"8080:18232"`.
+The server offers ESP32 firmware for the [web installer](/docs/agents/esp32#install-from-the-browser)
+and for [updates over the network](/docs/guides/agents#firmware-updates). Firmware only comes from
+the GitHub releases of `NETIGNITE_FIRMWARE_REPO`; there are no uploads.
-## Data paths
+A sync reads the repository's releases and keeps the 5 newest ones plus the newest stable release
+in `NETIGNITE_FIRMWARE_DIR`. A release is used if:
-All paths are relative to the working directory of the server process (`/opt/netignite` in
-Docker).
+- its tag is a version like `v2.0.0` or `v2.1.0-beta.1`,
+- it has a `release.json` for the agent protocol 2 and all files it lists,
+- every file matches the size and SHA-256 in `release.json` (files up to 4 MB),
+- with `NETIGNITE_FIRMWARE_PUBLIC_KEY` set: its `firmware.sig` is a valid signature of the app image.
-| Path | Content |
+Skipped releases appear in the server log and in the toast after **Sync now**. Releases that drop
+out of the newest ones are deleted. See [Release signing](/docs/agents/esp32#release-signing) for
+how releases are signed.
+
+## Data paths and backup
+
+| Path (Docker) | Content |
| --- | --- |
-| `data/db.sqlite` | SQLite database with users, agents and devices. Back this file up. |
-| `data/initial_admin_credentials` | Admin password generated on first start. Delete it after logging in. |
-| `config/config.json` | The config file above. |
+| `/opt/netignite/data/netignite.sqlite` | Database: users, sessions, API keys, agents, devices, shares, favorites, SSH logins, firmware list, settings. |
+| `/opt/netignite/data/firmware/` | Synced firmware. Can be deleted; the next sync downloads it again. |
+| `/opt/netignite/config/secrets.key` | Encrypts SSH passwords and private keys. Back it up with the database. |
+| `/opt/netignite/config/initial_admin_password_reset_token.txt` | The first admin's password link. Delete it after use. |
-The database schema is created automatically on startup. To back up the server, stop it and copy
-the `data/` directory.
+To back up the server, stop it and copy the `data/` and `config/` directories.
## Logging
-The server logs to stdout/stderr only. With Docker, read the logs with
-`docker logs netignite-server`.
+The server logs to stdout and stderr. With Docker, read the log with
+`docker logs netignite-server`. Lines look like this:
+
+```text [Server log]
+[2026-10-21T09:12:03.512Z] [INFO] Agent 3 online (esp32 2.0.0)
+```
| Level | Used for |
| --- | --- |
| `debug` | Detailed diagnostics |
-| `info` | Startup steps, agent connects and disconnects |
-| `warn` | Recoverable problems |
-| `error` | Failed operations, WebSocket errors |
+| `info` | Startup steps, agents connecting and disconnecting, enrollments, firmware syncs and updates |
+| `warn` | Recoverable problems, skipped firmware releases, unresponsive agents |
+| `error` | Failed operations |
| `critical` | Errors that stop the server |
::note
-The initial admin password is logged at level `info`. With `logLevel` set to `warn` or higher,
-it is only written to `data/initial_admin_credentials`.
+The first admin's password link is logged at level `info`. With `NETIGNITE_LOG_LEVEL` set to `warn`
+or higher, it's only in `initial_admin_password_reset_token.txt`.
::
diff --git a/content/docs/server/installation.md b/content/docs/server/installation.md
index 354083e..63d2364 100644
--- a/content/docs/server/installation.md
+++ b/content/docs/server/installation.md
@@ -16,10 +16,11 @@ ghcr.io/netignite-project/server
| Tag | Content |
| --- | --- |
| `latest` | The latest stable release |
-| `0.1.3`, `0.1.2`, … | A specific release (see the [release list](https://github.com/NetIgnite-project/Server/releases)) |
+| `2.0.0`, … | A specific release (see the [release list](https://github.com/NetIgnite-project/Server/releases)) |
+| `2.1.0-beta.1`, … | Pre-releases. They never move `latest`. |
Images are built for `linux/amd64` and signed with [cosign](https://github.com/sigstore/cosign)
-(keyless).
+(keyless). On other platforms, [run the server from source](#from-source).
## Docker Compose (recommended)
@@ -31,6 +32,8 @@ services:
restart: unless-stopped
ports:
- "18232:18232"
+ environment:
+ NETIGNITE_APP_URL: https://netignite.example.com
volumes:
- /opt/netignite/data:/opt/netignite/data
- /opt/netignite/config:/opt/netignite/config
@@ -41,9 +44,9 @@ docker compose up -d
docker compose logs -f netignite-server
```
-The upstream [compose file](https://github.com/NetIgnite-project/Server/blob/main/docker/docker-compose.yml)
-also mounts `/opt/netignite/logs`. The current release logs to stdout only (use `docker logs`),
-so that mount is optional.
+`NETIGNITE_APP_URL` is required: it's the address users open the dashboard at, and the server
+stops at startup without it. All other settings have defaults; see
+[Configuration](/docs/server/configuration).
## docker run
@@ -52,6 +55,7 @@ docker run -d \
--name netignite-server \
--restart unless-stopped \
-p 18232:18232 \
+ -e NETIGNITE_APP_URL=https://netignite.example.com \
-v /opt/netignite/data:/opt/netignite/data \
-v /opt/netignite/config:/opt/netignite/config \
ghcr.io/netignite-project/server:latest
@@ -61,69 +65,95 @@ docker run -d \
| Path | Content |
| --- | --- |
-| `/opt/netignite/data/db.sqlite` | The SQLite database (users, agents, devices) |
-| `/opt/netignite/data/initial_admin_credentials` | The generated admin password from the first start |
-| `/opt/netignite/config/config.json` | Server settings; created from a sample on first start (see [Configuration](/docs/server/configuration)) |
+| `/opt/netignite/data/netignite.sqlite` | The SQLite database |
+| `/opt/netignite/data/firmware/` | ESP32 firmware releases downloaded from GitHub |
+| `/opt/netignite/data/logs/` | Log files of background tasks |
+| `/opt/netignite/config/secrets.key` | Key that encrypts stored SSH passwords and private keys, created on first use |
+| `/opt/netignite/config/initial_admin_password_reset_token.txt` | The password link for the first admin |
+| `/opt/netignite/app/` | The application (part of the image, not a volume) |
-The container listens on port `18232` (`NITRO_PORT`) on all interfaces. The dashboard, the REST
-API and the agent WebSocket (`/api/agent-control-service`) all share this port.
+The container listens on port `18232` on all interfaces (IPv4 and IPv6). The dashboard, the REST
+API, the OpenAPI reference (`/api/docs/v1`) and the WebSockets `/ws/agent` and `/ws/console` all
+share this port. Docker checks the container's health with `GET /api/health`.
+
+Back up both volumes. Without `secrets.key`, stored SSH logins can't be decrypted.
## First start
On first start, the server:
-1. creates `config/config.json` from the bundled sample (an empty `{}`) if it doesn't exist,
-2. creates the database tables,
-3. creates the user `admin` with a random 64-character password, writes it to
- `data/initial_admin_credentials` and prints it to the log,
-4. loads the agents from the database and starts accepting agent connections.
+1. creates the database and applies the migrations,
+2. creates the user `admin` with a random password, and writes a link for setting its password to
+ `config/initial_admin_password_reset_token.txt` (and to the log),
+3. starts the API and accepts agent connections,
+4. downloads the newest ESP32 firmware releases from GitHub in the background (again every 6
+ hours).
```bash [Terminal]
-sudo cat /opt/netignite/data/initial_admin_credentials
+sudo cat /opt/netignite/config/initial_admin_password_reset_token.txt
```
+The link is valid for 7 days. Open it, set a password and log in as `admin`. See
+[Users & Accounts](/docs/server/users#the-initial-admin).
+
+The server needs outbound HTTPS to `api.github.com` and GitHub's download servers for the firmware
+sync. Without it, the ESP32 web installer has no firmware to offer; see
+[Configuration](/docs/server/configuration#firmware-sync).
+
::warning
-Log in and change the admin password right away, then delete `initial_admin_credentials`. The
-server never removes the file itself.
+Logging in and the ESP32 web installer need HTTPS: the browser only keeps the login cookie and
+only allows USB access on HTTPS pages (or on `localhost`). Before you use the dashboard from another
+machine, set up [HTTPS & a reverse proxy](/docs/server/reverse-proxy).
::
-Production builds only accept the session cookie over HTTPS (or on `localhost`). Before you use
-the dashboard from another machine, set up [HTTPS & a reverse proxy](/docs/server/reverse-proxy).
-
## Updating
-Pull the new image and recreate the container. The database and config live in the mounted
-volumes and are kept.
+Pull the new image and recreate the container. The database and the config live in the mounted
+volumes and are kept; database migrations run automatically on startup.
```bash [Terminal]
docker compose pull
docker compose up -d
```
-::note
-Login sessions are held in memory, so everyone has to log in again after a restart or update.
-::
+Logins are stored in the database, so users stay logged in. Agents reconnect on their own within a
+minute.
+
+Upgrading from NetIgnite v1 (`0.1.x`)? Follow [Upgrading from v1](/docs/server/upgrading).
## From source
-The server requires [Bun](https://bun.sh). Node.js is not supported: the build uses Nitro's `bun`
-preset and the startup code uses Bun APIs.
+The server requires [Bun](https://bun.sh); it uses Bun's runtime APIs and doesn't run on Node.js.
```bash [Terminal]
git clone https://github.com/NetIgnite-project/Server.git
cd Server
bun install
bun run build
-bun run .output/server/index.mjs
```
-Run the last command from the repository root: `config/` and `data/` are resolved relative to the
-working directory, and the config file is created from `config/config.sample.json`. Without
-`NITRO_PORT` the server listens on port `3000`; set `NITRO_PORT=18232` to match the Docker image.
+Create a `.env` file in the repository root. Bun loads it when it starts the server:
+
+```bash [.env]
+NETIGNITE_APP_URL=https://netignite.example.com
+```
+
+Then start the server from the repository root:
-To build the image yourself, use the repository root as the build context:
+```bash [Terminal]
+bun run start
+```
+
+`bun run start` listens on port `18232` (it sets `PORT=18232`). The data paths (`./data`,
+`./config`) and the database migrations (`./drizzle/migrations`) are resolved relative to the
+working directory. `bun run build` also writes `.output/import-v1.mjs`, the
+[v1 import](/docs/server/upgrading).
+
+To build the Docker image yourself, build the app first, then the image with the repository root as
+the build context:
```bash [Terminal]
+bun run build
docker build -f docker/Dockerfile -t netignite-server .
```
diff --git a/content/docs/server/reverse-proxy.md b/content/docs/server/reverse-proxy.md
index b71f040..5bb9f30 100644
--- a/content/docs/server/reverse-proxy.md
+++ b/content/docs/server/reverse-proxy.md
@@ -1,27 +1,38 @@
---
title: "HTTPS & Reverse Proxy"
-description: "Serve the NetIgnite dashboard and the agent WebSocket over HTTPS with Caddy, nginx or Apache."
+description: "Serve the NetIgnite dashboard, API and WebSockets over HTTPS with Caddy, nginx or Apache."
navigation:
title: HTTPS & Reverse Proxy
---
# HTTPS & Reverse Proxy
-The NetIgnite server speaks plain HTTP. Put it behind a reverse proxy that terminates TLS before
-you use it outside `localhost`:
+The NetIgnite server speaks plain HTTP on port `18232`. Put it behind a reverse proxy that
+terminates TLS before you use it outside `localhost`:
-- **Logins need HTTPS.** Production builds mark the `session` cookie as `Secure`. Browsers only
- store it over HTTPS or on `localhost`, so logging in over `http://:18232` fails.
-- **Agent credentials.** Agents send their ID and secret in an HTTP Basic `Authorization` header.
- Without TLS, anyone on the path can read them.
+- **Logins need HTTPS.** The dashboard stores its login token in a `Secure` cookie, which browsers
+ only keep on HTTPS pages or on `localhost`. Over `http://:18232`, every login ends on
+ the login page again.
+- **The ESP32 web installer needs HTTPS.** Browsers only allow USB (Web Serial) access on HTTPS
+ pages or on `localhost`.
+- **Credentials.** Agents send their credential in an `Authorization` header; the dashboard sends
+ its login token and passwords. Without TLS, anyone on the path can read them.
-The dashboard, the REST API and the agent WebSocket share one port, so a single proxy rule covers
-all of them. The proxy must:
+Set `NETIGNITE_APP_URL` to the public HTTPS address, for example
+`https://netignite.example.com` (see [Configuration](/docs/server/configuration#the-public-url)).
+Serve NetIgnite at the root of its own host name; sub-paths aren't supported.
-1. forward WebSocket upgrades for `/api/agent-control-service`,
-2. pass the `Authorization` header through (all proxies below do this by default),
-3. allow idle connections longer than 60 seconds. The server sends each agent a heartbeat once a
- minute, so a 60-second idle timeout can cut agent connections.
+## What the proxy must do
+
+The dashboard, the API and both WebSockets share one port, so a single proxy rule covers all of
+them. The proxy must:
+
+1. forward WebSocket upgrades: `/ws/agent` (agents) and `/ws/console` (the live log of an ESP32
+ agent in the dashboard),
+2. pass the `Authorization`, `Host` and `Origin` headers through (the examples below do),
+3. keep idle WebSockets open for at least 60 seconds. The server pings every agent every 30
+ seconds. The console can be quiet for longer; the dashboard reconnects it after 5 seconds if the
+ proxy closes it.
The examples assume NetIgnite runs on the same host at `127.0.0.1:18232`, published as
`netignite.example.com`.
@@ -61,9 +72,9 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
- # Agents may be idle for a minute between heartbeats.
- proxy_read_timeout 300s;
- proxy_send_timeout 300s;
+ # WebSockets: agents get a ping every 30 s, the console can be quiet for longer.
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
}
}
```
@@ -81,28 +92,39 @@ Requires Apache 2.4.47 or newer with `mod_proxy`, `mod_proxy_http` and `mod_ssl`
SSLCertificateKeyFile /etc/letsencrypt/live/netignite.example.com/privkey.pem
ProxyPreserveHost On
- ProxyTimeout 300
+ ProxyTimeout 600
ProxyPass / http://127.0.0.1:18232/ upgrade=websocket
ProxyPassReverse / http://127.0.0.1:18232/
```
-## Agent settings behind a proxy
+## Agents behind a proxy
-Point your agents at the public hostname on port `443` with TLS enabled. For the ESP32 agent:
+Agents use the server address from their setup: the ESP32 web installer and the Linux agent's
+setup command fill in `NETIGNITE_APP_URL`. Agents connect to `wss://` for `https://` addresses and
+to `ws://` for `http://` addresses.
-```cpp [config.h]
-#define C_CONTROL_SERVER_HOST "netignite.example.com"
-#define C_CONTROL_SERVER_PORT (uint16_t) 443
-#define C_CONTROL_SERVER_USE_SSL (bool) true
-```
+- The **ESP32 agent** checks the server's certificate against Mozilla's list of public root CAs
+ (Let's Encrypt included). A certificate from a private CA only works with a firmware you
+ [build yourself](/docs/agents/esp32#build-from-source) with that CA added.
+- The **Linux agent** checks the certificate against the public root CAs built into its runtime
+ ([Bun](https://bun.sh)). Use a certificate from a public CA such as Let's Encrypt.
+
+An agent in the same LAN as the server can also use a plain `http://` address such as
+`http://192.168.1.10:18232`: set it with **Change server address** in the web installer, or pair
+the Linux agent with it. Its credential then crosses the network unencrypted, so only do this in a
+network you trust.
+
+## Rate limits behind a proxy
-The ESP32 agent verifies the server certificate against a bundle of public root CAs (including
-Let's Encrypt). Self-signed certificates or a private CA only work if you
-[add your CA to the bundle](/docs/agents/esp32#certificate-bundle).
+The server counts failed logins, sign-ups and agent enrollments per client IP address. It uses the
+address of the TCP connection and doesn't read `X-Forwarded-For`, so behind a proxy all clients
+share the proxy's address for these limits.
## Checking the setup
1. Open `https://netignite.example.com` and log in.
-2. Start an agent and watch the server log for `Agent '' connected`.
-3. If the agent connects and drops again after about a minute, raise the proxy's idle timeout.
+2. Open `https://netignite.example.com/api/health`; it answers
+ `{"success": true, "code": 200, "message": "NetIgnite API is running", …}`.
+3. Start an agent and watch the server log for `Agent online (…)`.
+4. Open an ESP32 agent's **Console** tab: the badge shows **Live** when `/ws/console` works.
diff --git a/content/docs/server/upgrading.md b/content/docs/server/upgrading.md
new file mode 100644
index 0000000..543b33d
--- /dev/null
+++ b/content/docs/server/upgrading.md
@@ -0,0 +1,165 @@
+---
+title: "Upgrading from v1"
+description: "Move a NetIgnite v1 server to v2: back up, run v2 next to the v1 data, import users, agents and devices, and set the agents up again."
+navigation:
+ title: Upgrading from v1
+---
+
+# Upgrading from v1
+
+NetIgnite v2 is a rewrite of the server and the agents. Your v1 data is imported once into the new
+database; the v1 database stays untouched. Agents have to be set up again: v1 agents can't connect
+to a v2 server, and ESP32 boards need the v2 firmware.
+
+::warning
+**Using the `latest` image?** Since 2.0.0, `ghcr.io/netignite-project/server:latest` is v2. A v1
+server that pulls it, by hand or with an auto-updater such as Watchtower, starts v2 with an empty
+database next to the v1 one: users, agents and devices seem to be gone, but they're still in
+`data/db.sqlite`, and the server log says *Found a NetIgnite v1 database … that hasn't been
+imported*. Continue at [Try the import](#try-the-import). To stay on v1 for now, use the image
+`ghcr.io/netignite-project/server:0.1.3`.
+::
+
+| | v1 (`0.1.x`) | v2 |
+| --- | --- | --- |
+| Database | `data/db.sqlite` | `data/netignite.sqlite` (the import reads `db.sqlite`) |
+| First admin | Password in `data/initial_admin_credentials` | Password link in `config/initial_admin_password_reset_token.txt` |
+| Settings | `config/config.json` | `NETIGNITE_*` environment variables (`NETIGNITE_APP_URL` is required) |
+| Agent connection | `/api/agent-control-service`, ID and secret from the dashboard | `/ws/agent`, credential generated by the agent at enrollment |
+| ESP32 setup | `config.h`, built with PlatformIO | Web installer in the dashboard, settings in flash |
+| Port, image, volumes | `18232`, `ghcr.io/netignite-project/server`, `/opt/netignite/data` and `/opt/netignite/config` | unchanged |
+
+## What is migrated
+
+| v1 | In v2 |
+| --- | --- |
+| Users | Imported with their username, password and role. They get a placeholder email address `@imported.netignite.invalid`. Passwords are converted to the new hash format at the first login. |
+| The v1 `admin` | Merged into the v2 `admin`. If nobody has set the v2 `admin` password yet (the password link is unused), it takes over the v1 password and the link stops working. |
+| Agents | Imported with name and description, but **not set up** (status *Not set up*): v1 secrets can't be used in v2. *Microcontroller* agents become ESP32 agents, *Server* agents become Linux agents. |
+| Devices | Imported with name, type, description, MAC address, Wake-on-LAN port and agent. Their status check is **MAC (IPv6 link-local)**, the check v1 used. Devices without an agent get no status check. |
+| Favorites | Imported. |
+
+Not migrated: v1 agent secrets, v1 logins (users log in again), `config/config.json` (set
+`NETIGNITE_LOG_LEVEL` instead) and the v1 admin password file. Devices with an invalid MAC address
+are skipped and listed in the import's output.
+
+::steps{level="2"}
+
+## Back up
+
+Stop the v1 server and copy its data and config:
+
+```bash [Terminal]
+docker compose down
+sudo cp -a /opt/netignite /opt/netignite-v1-backup
+```
+
+## Start v2 next to the v1 data
+
+Change the image to v2 and add `NETIGNITE_APP_URL` with the address users open the dashboard at.
+Keep the volumes:
+
+```yaml [docker-compose.yml]
+services:
+ netignite-server:
+ image: ghcr.io/netignite-project/server:latest
+ container_name: netignite-server
+ restart: unless-stopped
+ ports:
+ - "18232:18232"
+ environment:
+ NETIGNITE_APP_URL: https://netignite.example.com
+ volumes:
+ - /opt/netignite/data:/opt/netignite/data
+ - /opt/netignite/config:/opt/netignite/config
+```
+
+```bash [Terminal]
+docker compose pull
+docker compose up -d
+```
+
+v2 creates its own database `data/netignite.sqlite` next to the v1 `data/db.sqlite`, and a new
+`admin` with a password link. Don't use the link yet if you want the v1 admin password to carry over.
+
+If you use a reverse proxy, check that it forwards WebSockets on `/ws/agent` and `/ws/console`
+(v1 only needed `/api/agent-control-service`). See [HTTPS & Reverse Proxy](/docs/server/reverse-proxy).
+
+## Try the import
+
+A dry run imports everything inside a transaction and rolls it back, so nothing changes:
+
+```bash [Terminal]
+docker exec netignite-server bun /opt/netignite/app/.output/import-v1.mjs \
+ --from /opt/netignite/data/db.sqlite --dry-run
+```
+
+```text [Output]
+Dry run — nothing was changed.
+
+Users imported: 2
+Users merged: admin
+Agents imported: 2
+Devices imported: 5
+Favorites imported: 3
+Imported users got a placeholder email (…@imported.netignite.invalid) and are asked to change it after logging in.
+```
+
+Users whose username already exists in v2 (other than `admin`) are skipped:
+`skipped user alice: username already exists in v2 (use --merge-users to merge them)`. With
+`--merge-users`, such a v1 user is merged into the existing v2 user instead; the v2 user takes over
+the v1 password only if nobody has logged in to it in v2 yet.
+
+## Import
+
+```bash [Terminal]
+docker exec netignite-server bun /opt/netignite/app/.output/import-v1.mjs \
+ --from /opt/netignite/data/db.sqlite
+```
+
+The import uses the container's `NETIGNITE_*` settings and writes to the running server's
+database. Running it again is safe: records that were already imported are skipped. When the
+`admin` takes over the v1 password, the import deletes `config/initial_admin_password_reset_token.txt`.
+
+Running the server from source? Run the import from the repository root:
+`bun scripts/import-v1.ts --from [--dry-run] [--merge-users]`.
+
+## Log in
+
+Log in as `admin` with the v1 password (or, if you set a v2 password with the link before the
+import, with that one). Every user gets the welcome page at the first login; ask your users to enter their real
+email address under **Settings → General**.
+
+## Set up the agents again
+
+Open **Agents**. The imported agents show **Not set up**; their devices still point at them.
+
+- **ESP32 boards** need the v2 firmware once over USB: open the agent's **Install** tab and follow
+ the [web installer](/docs/agents/esp32#install-from-the-browser). v2 uses a new partition table
+ with two firmware slots, so it can't be installed over the network on a v1 board. After that,
+ updates come [over the network](/docs/guides/agents#firmware-updates). Your old `config.h` isn't
+ needed anymore: the installer sets the server and the Wi-Fi.
+- **Server agents** (your own v1 agents) don't work with v2: the
+ [protocol](/docs/agents/protocol) changed. Install the [Linux agent](/docs/agents/linux) on the
+ machine instead (turn on Wake-on-LAN mode to wake other devices), or update your agent to
+ protocol v2.
+
+As each agent comes online, its devices can be woken again.
+
+## Check the devices
+
+The imported devices keep the v1 status check, which doesn't work for most Windows and many Linux
+machines. Open each device and choose a better [status check](/docs/guides/devices#status-checks),
+for example its IPv4 address. While you're there, consider [sharing](/docs/guides/sharing),
+shutdown through the [Linux agent](/docs/agents/linux#host-mode) or over [SSH](/docs/guides/ssh).
+
+::
+
+## Going back
+
+The v1 database `data/db.sqlite` is never modified. To go back, run the v1 image again (for example
+`ghcr.io/netignite-project/server:0.1.3`) with the same volumes. Agents you set up for v2 don't
+work with v1: flash the v1 firmware onto ESP32 boards again.
+
+Once you're happy with v2, you can delete `data/db.sqlite`, `data/initial_admin_credentials` and
+`config/config.json`.
diff --git a/content/docs/server/users.md b/content/docs/server/users.md
index 91832b6..ae7b0a0 100644
--- a/content/docs/server/users.md
+++ b/content/docs/server/users.md
@@ -1,6 +1,6 @@
---
title: "Users & Accounts"
-description: "The initial admin account, login sessions, roles and password changes on a self-hosted NetIgnite server."
+description: "The first admin, user management, public sign-up, roles, logins, password resets and API keys on a NetIgnite server."
navigation:
title: Users & Accounts
---
@@ -14,44 +14,107 @@ When the server starts with an empty database, it creates one account:
| Field | Value |
| --- | --- |
| Username | `admin` |
-| Password | 64 random hex characters, written to `data/initial_admin_credentials` and printed to the log |
+| Display name | Default Administrator |
+| Email | `admin@netignite.dev` (a placeholder; change it) |
| Role | `admin` |
+| Password | Random and unknown. Set it with the link below. |
-```text [initial_admin_credentials]
-Username: admin
-Password: 3f9c…
+The server writes a password link to `config/initial_admin_password_reset_token.txt` (in Docker:
+`/opt/netignite/config/…`, readable only by root) and prints it to the log:
+
+```text [initial_admin_password_reset_token.txt]
+https://netignite.example.com/auth/reset-password?token=3f9c…
```
-Log in, open **Settings** from the user menu, set a new password and delete the credentials file.
+1. Open the link within **7 days** and choose a password.
+2. Log in with the username `admin` and that password.
+3. The welcome page opens. Click **Complete your profile** (or go to **Settings → General**) and
+ enter your real email address and name. Saving changes there asks for your current password.
+4. Delete `initial_admin_password_reset_token.txt`. The link stops working once it's used, but the
+ server doesn't remove the file.
-## Changing username and password
+The link uses `NETIGNITE_APP_URL`. If that was wrong at the first start, replace the address in the
+link and keep the `?token=…` part.
-Every user can change their own username and password under **Settings** (`/account/settings`).
-Leave the password field empty to keep the current password. Usernames must be unique.
+::note
+If the link expired before you used it and the server has no data yet, stop the server, delete
+`data/netignite.sqlite` and start it again: an empty database gets a new admin and a new link. With
+data, see [I lost the admin password](/docs/reference/troubleshooting#i-lost-the-admin-password).
+::
-Changing the password does not end other active sessions.
+## Roles
-## Sessions
+There are two roles: `admin` and `user`.
-- Logging in sets a `session` cookie that is valid for 30 days. The cookie is `HttpOnly` (scripts
- on the page can't read it), `SameSite=Strict` and, in production builds, `Secure`. Serve the
- dashboard over [HTTPS](/docs/server/reverse-proxy).
-- Sessions are kept in server memory. Restarting or updating the server logs everyone out.
-- **Log out** in the user menu ends the session immediately.
+- Agents and devices belong to the user who created them, for both roles. Admins don't see other
+ users' agents or devices. Users see devices that were [shared](/docs/guides/sharing) with them.
+- Admins additionally get the **Admin** section in the sidebar: **Users** and **Server Settings**.
-## Roles
+## Managing users
-There are two roles: `admin` and `user`.
+Admins manage accounts under **Admin → Users**:
-- Agents and devices are private to the user who created them, for both roles. Admins don't see
- other users' agents or devices.
-- Through the [API](/docs/reference/api#update-an-account), admins can edit other users' usernames
- and passwords. The first user (the initial `admin`, ID `1`) can also change other users' roles.
+- **New User** creates an account with a username, display name, email address, password and role.
+- **Edit** changes the display name, email address and role. A changed role applies to the user's
+ existing logins and API keys right away.
+- **Reset Password** sets a new password and logs the user out everywhere.
+- **Delete** removes the account with all its data: its agents, devices, shares, API keys and
+ logins.
-## Adding users
+Usernames have 5 to 40 characters: lowercase letters, digits, `.`, `_` and `-`, starting with a
+letter or digit. Usernames and email addresses must be unique.
-::note
-The current release has no user management in the dashboard, no self-registration and no
-password reset. A self-hosted server starts with the single `admin` account, which is enough to
-manage all of your own agents and devices.
-::
+## Public sign-up
+
+By default only admins create accounts. To let people register themselves, turn on
+**Admin → Server Settings → Registration → Public sign-up**. The login page then shows a **Sign up**
+link to `/auth/signup`, where anyone who can open the server creates an account with the role
+`user`. The server accepts at most 5 sign-ups per client address and hour.
+
+## Logging in
+
+- A login is valid for **7 days**. The dashboard keeps it in the cookie
+ `netignite_session_token`, which requires [HTTPS](/docs/server/reverse-proxy) (or `localhost`).
+ With **Remember me**, the browser keeps it after it's closed.
+- Logins are stored in the database, so restarting or updating the server doesn't log anyone out.
+- **Log out** in the user menu ends the login on the server.
+- After 5 failed attempts for a username from one address (or 15 from all addresses), further
+ logins for it are refused until 5 minutes after the first attempt.
+
+## Changing your account
+
+Under **Settings** in the sidebar:
+
+- **General**: username, display name and email address. Saving asks for your current password.
+- **Security**: change your password (this logs you out everywhere) or delete your account. Deleting
+ your account deletes your agents and devices too.
+- **API Keys**: see [below](#api-keys).
+
+Passwords need 8 to 50 characters with an uppercase letter, a lowercase letter, a digit and a
+special character, everywhere: sign-up, **Create User**, **Change Password**, an admin's **Reset
+Password** and reset links.
+
+## Forgotten passwords
+
+**Forgot password?** on the login page sends a reset link to the account's email address. The link
+is valid for 1 hour; one link per address is sent every 15 minutes. This needs
+[email](/docs/server/configuration#email) to be configured. Without email, an admin resets the
+password under **Admin → Users**.
+
+## API keys
+
+API keys let scripts and integrations use the [REST API](/docs/reference/api) as you. Create one
+under **Settings → API Keys → New API Key** with a description and an expiration (7, 30, 90, 180 or
+365 days, or none). The key (`netignite_apikey_…`) is shown **once**; copy it right away. Send it as
+`Authorization: Bearer `.
+
+An API key can do what you can do with agents, devices and firmware, and admin tasks if you're an
+admin. Your own account (profile, password, API keys) can only be changed with a login, not with an
+API key. Delete a key under **Settings → API Keys**; it stops working immediately. Keys of a user
+whose role changes get the new role.
+
+## Imported v1 users
+
+Users imported from NetIgnite v1 keep their username and password. They get a placeholder email
+address (`@imported.netignite.invalid`) and should enter their real one under
+**Settings → General** after their first login. See [Upgrading from v1](/docs/server/upgrading).
diff --git a/nuxt.config.ts b/nuxt.config.ts
index fb76169..6f127a4 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -61,7 +61,7 @@ export default defineNuxtConfig({
build: {
markdown: {
highlight: {
- langs: ["bash", "json", "yaml", "ts", "cpp", "nginx", "apache", "http"],
+ langs: ["bash", "json", "yaml", "ts", "js", "ini", "nginx", "apache", "http"],
},
},
},
diff --git a/public/static/utils/sitemap.xml b/public/static/utils/sitemap.xml
index 6f5d5ae..9b6ed5d 100644
--- a/public/static/utils/sitemap.xml
+++ b/public/static/utils/sitemap.xml
@@ -21,77 +21,97 @@
https://netignite.dev/docs
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.9
https://netignite.dev/docs/how-it-works
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.8
https://netignite.dev/docs/getting-started
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.8
https://netignite.dev/docs/server/installation
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.8
https://netignite.dev/docs/server/configuration
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/server/reverse-proxy
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/server/users
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
+ 0.7
+
+
+ https://netignite.dev/docs/server/upgrading
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/guides/dashboard
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/guides/agents
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/guides/devices
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
+ 0.7
+
+
+ https://netignite.dev/docs/guides/sharing
+ 2026-10-21T00:00:00+00:00
+ 0.7
+
+
+ https://netignite.dev/docs/guides/ssh
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/agents/esp32
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
+ 0.8
+
+
+ https://netignite.dev/docs/agents/linux
+ 2026-10-21T00:00:00+00:00
0.8
https://netignite.dev/docs/agents/protocol
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/reference/api
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/reference/troubleshooting
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.7
https://netignite.dev/docs/reference/development
- 2026-10-18T00:00:00+00:00
+ 2026-10-21T00:00:00+00:00
0.6