Overview
The Nova Partner API lets your own backend control the Nova devices that belong to your account: a booking system that switches a studio on when a session starts, a dashboard that shows live state, a scheduler that dims lights at night. Your server holds an API key, calls a small REST API, and receives signed webhooks when something changes.
Three ideas carry the whole API:
- A key acts as the owner. It can see and command every device the owning account holds, and nothing else. There is no cross-account sharing anywhere.
- Devices are controlled through channels. Every controllable point on a device is a numbered channel. What a channel can do is described by its capabilities, discovered at runtime. There is no per-product documentation and never will be.
- Webhooks notify, the API confirms. Events tell you something changed; the state endpoint is the source of truth.
| Base URL | Format | Auth |
|---|---|---|
https://api.novacube.io/v1 | JSON over HTTPS; snake_case keys; ignore unknown keys | Authorization: Bearer nova_pk_… |
http://localhost:8080/v1. The production host above is reserved and goes live with the first deployment.Getting started in five calls
- In the Nova app, open Account → Developer settings and switch it on. Under API keys create a key with the scopes you need. The secret is shown once; store it in your server's secret store.
- Check the key:
GET /v1/key. - List your devices:
GET /v1/devices. - Discover what a device can do:
GET /v1/devices/{device_id}/channels. - Command it:
POST /v1/devices/{device_id}/commands, and read it back withGET …/state.
# 1. who am I?
curl https://api.novacube.io/v1/key \
-H "Authorization: Bearer nova_pk_k7m2x9pqab_…"
# 2. my devices
curl https://api.novacube.io/v1/devices \
-H "Authorization: Bearer $NOVA_KEY"
# 3. what can device 4f1d… do?
curl https://api.novacube.io/v1/devices/4f1d580f-c9bc-4bd0-adea-cc69e042c43b/channels \
-H "Authorization: Bearer $NOVA_KEY"
# 4. switch channel 2 on at 60 %
curl -X POST https://api.novacube.io/v1/devices/4f1d580f-c9bc-4bd0-adea-cc69e042c43b/commands \
-H "Authorization: Bearer $NOVA_KEY" -H "Content-Type: application/json" \
-d '{"cmd_id":"booking-8841-start","channel":2,"set":{"on":true,"level":60}}'
# 5. confirm
curl https://api.novacube.io/v1/devices/4f1d580f-c9bc-4bd0-adea-cc69e042c43b/state \
-H "Authorization: Bearer $NOVA_KEY"Then register a webhook endpoint in the app so your backend hears about changes without polling. The rest of this page explains each step.
Java SDK
For a Java backend, the Nova IoT Java SDK wraps everything on this page: paging, cmd_id generation and retries, typed errors, stream-key sealing and webhook verification. Every section below shows the raw HTTP first and the Java call next to it. Java 17 or newer.
| Artifact | What |
|---|---|
com.nova.iot:nova-iot-sdk | The client. No Spring: the JDK's HTTP client and Jackson 2 (jackson-databind) only |
com.nova.iot:nova-iot-sdk-spring-boot-starter | Optional, Spring Boot 3: builds a NovaClient and a webhook verifier from nova.* properties |
Install
You receive the SDK from Nova as one file, nova-iot-sdk-0.2.0-bundle.zip: the jars with their sources and Javadoc, an install script and INSTALL.md. No repository login. Unzip it and run the script; it installs the SDK into your local Maven repository (~/.m2):
unzip nova-iot-sdk-0.2.0-bundle.zip
cd nova-iot-sdk-0.2.0-bundle
./install.sh # Windows: install.cmdThen add the dependency (Maven reads ~/.m2 first):
<dependency>
<groupId>com.nova.iot</groupId>
<artifactId>nova-iot-sdk</artifactId> <!-- or nova-iot-sdk-spring-boot-starter -->
<version>0.2.0</version>
</dependency>Gradle: add mavenLocal() to repositories and implementation("com.nova.iot:nova-iot-sdk:0.2.0"). The only other dependency, Jackson 2 jackson-databind, comes from Maven Central as usual.
The client
import com.nova.iot.sdk.NovaClient;
import com.nova.iot.sdk.generated.model.DeviceResponse;
NovaClient nova = NovaClient.builder()
.apiKey(System.getenv("NOVA_KEY")) // nova_pk_<key_id>_<secret>
.baseUrl("https://api.novacube.io") // default; or http://localhost:8080
.build();
// GET /v1/key: a good health check
System.out.println(nova.keys().current().getScopes());
// fetches every page as you iterate
for (DeviceResponse d : nova.devices().list()) {
System.out.println(d.getSerial() + " online=" + d.getOnline());
}NovaClientis immutable and thread-safe: build it once and share it. Builder options:apiKey(required),baseUrl,connectTimeout(10 s),requestTimeout(30 s per attempt),retryPolicy,httpClient(a proxy or SSL context),objectMapper,userAgent.- Resources:
devices(),commands(),streaming(),kiosk(),keys(). They return the models generated from this API's OpenAPI document (com.nova.iot.sdk.generated.model: getters such asgetSerial(),Instanttimes, unknown fields ignored). Every id is ajava.util.UUID.
Spring Boot starter
With the starter on the class path, these properties give you a NovaClient bean (when nova.api-key is set) and a NovaWebhooks bean (when at least one secret is set). Define your own bean of either type and the starter steps aside. Your application chooses the Spring Boot 3 version; the SDK keeps its own JSON mapper, so your Jackson settings never change how the API is read.
nova:
api-key: ${NOVA_KEY}
base-url: https://api.novacube.io
connect-timeout: 10s
request-timeout: 30s
retry:
max-attempts: 3
initial-backoff: 500ms
max-backoff: 5s
wait-on-rate-limit: true
max-retry-after: 60s
webhook-secrets:
- ${NOVA_WEBHOOK_SECRET}
webhook-tolerance: 5m@Service
class Studio {
private final NovaClient nova;
Studio(NovaClient nova) { this.nova = nova; } // injected by the starter
}Authentication and scopes
Every request carries the key as a bearer token. A key looks like nova_pk_<key_id>_<secret>: the key_id is public (it appears in logs and in the audit trail of every command as src: partner:<key_id>); the secret is shown once at creation and stored hashed on our side.
GET /v1/devices HTTP/1.1
Host: api.novacube.io
Authorization: Bearer nova_pk_k7m2x9pqab_Vq0b3Jq1Zt8rYw2nXc5LmP7dHs9KfA4eScopes are chosen when a key is created and checked on every call. Give each integration the least it needs: a dashboard gets read, a booking backend gets command + read. Revoking one integration's key never touches another.
| Scope | Allows |
|---|---|
read | GET devices, channel discovery, state, GET …/stream, GET …/kiosk |
command | POST commands; POST …/stream, PUT/DELETE …/stream-destinations; POST …/kiosk/pin, POST …/kiosk/lock, PUT …/kiosk/visibility |
Authentication and authorization failures are problem responses (see Errors) with a stable code:
| Status | code | Meaning |
|---|---|---|
| 401 | api_key_required | No bearer token |
| 401 | api_key_invalid | Malformed, unknown, or wrong secret |
| 401 | api_key_revoked | Revoked by the owner or by Nova; create a new key |
| 401 | api_key_expired | A retiring key past its date; use its successor |
| 403 | insufficient_scope | The key lacks the scope this endpoint needs |
| 403 | partner_suspended | The owner suspended all keys (panic button); they can resume in the app |
| 403 | developer_access_revoked | Nova suspended the partner surface of this account; contact support |
| 429 | rate_limited | Per-key budget used up; honour Retry-After |
Where you are called from matters a little. The first request from a new network address notifies the owner in the app (no blocking, server moves are normal). Expect it; it is not an error.
API keys: create, rotate, retire, revoke
Keys are managed only by the device owner in the Nova app (Account → Developer settings → API keys). There is no API to create keys: whoever can create a key is, by definition, the owner. Each key has a label, a scope mask, a creation time and a last-used time, all visible in the app.
Create
Tap Create key, give it a label naming the integration, pick scopes. The app shows the secret once with Copy and a QR; after that only the key_id is visible. Store the secret in your server's secret manager, never in a repository or a client app.
Rotate without downtime
Rotate on a key creates a successor with the same scopes and puts the old key into a retiring state for a grace period (7 days by default). Both keys work during the window, so you deploy the new secret at your pace and nothing fails over the cutover. When the window ends the old key stops. If the old key is still receiving traffic a day before its end, the owner gets a warning in the app.
GET /v1/key (with the OLD key, during the window)
{
"key_id": "k7m2x9pqab",
"label": "Booking backend",
"scopes": ["command", "read"],
"status": "retiring",
"created_at": "2026-09-01T10:00:00Z",
"retires_at": "2026-10-17T12:00:00Z",
"account_id": "a1b2c3d4-…"
}Revoke
Revoke stops a key immediately and permanently. Use it for a leaked secret or a decommissioned integration. Calls with it answer 401 api_key_revoked.
What Nova may do
- Platform retirement. Nova can put a key into the same retiring state with a reason (security policy, a detected leak). The owner is notified by push and email with the date; rotate in the app before it.
- Emergency revoke on confirmed compromise, notified afterwards.
- Developer access can be suspended account-wide by Nova; every key answers
403 developer_access_revokeduntil restored. - The
nova_pk_prefix is registered with public secret-scanning programs. A key found in a public repository enters retirement automatically.
Owner controls
The owner's Suspend partner access (hold to confirm) pauses every key and webhook delivery at once; Resume restores them. Turning the Developer settings switch off only hides the section in the app: keys keep working.
Usage log
Every authenticated request is written to the key's usage log with the time, your server's address, the method and path, and the HTTP status Nova answered. The owner sees the same log in the app (per key, with a 24-hour request count); your key can read its own, newest first, for debugging a deployment:
GET /v1/key/usage?page=1&size=25
{
"items": [
{ "id": "…", "at": "2026-10-10T12:00:03Z", "ip": "198.51.100.7", "method": "POST", "path": "/v1/devices/8d6f…/commands", "status": 200 },
{ "id": "…", "at": "2026-10-10T11:59:58Z", "ip": "198.51.100.7", "method": "GET", "path": "/v1/devices", "status": 200 }
],
"page": 1, "size": 25, "total": 2
}Rows are kept for 30 days. The first request from a new network address also notifies the owner (a notice, never a block).
Devices
GET/v1/devicesscope: read
GET/v1/devices/{device_id}scope: read
The list is every device currently in the owning account, paged (?page=1&size=25, max 100), sorted by serial. A device that is not in the account answers 404 device_not_found, the same answer as a device that does not exist: keys cannot enumerate other fleets.
{
"items": [
{
"device_id": "4f1d580f-c9bc-4bd0-adea-cc69e042c43b",
"serial": "NVA-SB-54723-T",
"model": { "code": "SB/M1", "name": "Studio Box M1" },
"online": true,
"last_seen_at": "2026-10-10T07:41:12Z",
"channel_count": 6,
"added_at": "2026-10-09T18:02:40Z",
"blocked": false,
"control_mode": "APP_AND_INTEGRATIONS"
}
],
"page": 1, "size": 25, "total": 1
}| Field | Meaning |
|---|---|
device_id | Stable id; the one you persist and key automation on |
serial | The label serial; convenient for humans, do not derive anything from it |
online | Presence = the device's live connection to Nova. Commands need it; reads do not |
last_seen_at | Last time the device talked to Nova; null before its first connection |
added_at | When the device joined this account |
blocked | Blocked by Nova (support action). Commands answer 409 device_blocked until lifted |
control_mode | The owner's Remote control setting: APP_AND_INTEGRATIONS (default) or INTEGRATIONS_ONLY — your commands work; APP_ONLY — they answer 409 integration_control_off. See Remote control |
Java SDK
// lazy: fetches page after page as you iterate; also .stream()
for (DeviceResponse d : nova.devices().list()) { … }
// one page: items(), page(), size(), total(), hasNext()
Page<DeviceResponse> p = nova.devices().page(1, 25);
// NotFoundException (device_not_found) when the device is not yours
DeviceResponse d = nova.devices().get(deviceId);The owner's names for devices and channels are never returned. Keep your own labels keyed by device_id and channel number.
When a device enters or leaves the account (claimed, released, transferred away) you receive the device.access_granted / device.access_revoked webhooks with the device_id only.
Channel discovery
GET/v1/devices/{device_id}/channelsscope: read
Every controllable point on a device is a channel, numbered 1…n. Numbers are assigned at the factory and never change: safe to persist, safe to automate on. A channel's behaviour is the composition of its capabilities (the closed catalogue in the next section) with per-channel constraints. There is deliberately no channel "type": the capability set is the type.
[
{ "channel": 1, "kind": "switch", "capabilities": ["on_off"], "constraints": {}, "state": { "on": true } },
{ "channel": 2, "kind": "light",
"capabilities": ["on_off", "level", "color_temp"],
"constraints": { "level_range": [1, 100], "temp_range": [2700, 6500] },
"state": { "on": true, "level": 80, "temp": 3400 } },
{ "channel": 3, "kind": "light",
"capabilities": ["on_off", "color_rgb"],
"constraints": { "color_model": "rgbw" },
"state": {} },
{ "channel": 4, "kind": "source",
"capabilities": ["source_select"],
"constraints": { "sources": ["in_1", "in_2"] },
"state": { "source": "in_1" } }
]constraintsare the exact parameters the validator uses. Build your UI and validate your own requests from them; nothing has to be hard-coded per product.kind(switch · light · fan · socket · source · lock · sensor, additive) is a presentation hint, the human noun. It never affects which commands are valid; treat an unknown kind generically.stateis the current value of each field the channel carries, from Nova's projection. It is empty until the device has connected for the first time.
Java SDK
for (ChannelResponse ch : nova.devices().channels(deviceId)) {
if (ch.getCapabilities().contains("level")) {
List<?> range = (List<?>) ch.getConstraints().get("level_range"); // [1, 100]
System.out.println(ch.getChannel() + " dims " + range + ", now " + ch.getState());
}
}The integration loop: read the catalogue once → discover a device's channels → construct commands from the fields its capabilities allow, with values inside its constraints. The same code drives every Nova product, current and future.
Capability catalogue
Closed, versioned, platform-wide, additive-only. Capability ids and field names are lower_snake_case and stable forever. Each capability carries exactly one command field; state echoes the same field.
| Capability | Field | Type | Constraint | Valid when |
|---|---|---|---|---|
on_off | on | bool | — | type only |
level | level | int | level_range: [min, max] ⊆ 0–100, default [0, 100] | min ≤ v ≤ max |
color_temp | temp | int (kelvin) | temp_range: [min, max], always present | min ≤ v ≤ max |
color_rgb | rgb | object {r, g, b[, w]} of ints 0–255 | color_model: "rgb" | "rgbw", always present | exactly the model's components, each 0–255 |
source_select | source | string | sources: [ids…], always present | v ∈ sources |
position_select | position | string | positions: [ids…], always present | v ∈ positions — where the channel's picture sits (e.g. an inset corner) |
pulse_on | pulse_on (+ optional pulse_ms) | bool (+ int ms) | pulse_ms: [min, max], optional | rest on: on → off for pulse_ms → on (restart); off → on. true starts one; false is a no-op |
pulse_off | pulse_off (+ optional pulse_ms) | bool (+ int ms) | pulse_ms: [min, max], optional | rest off: on → off; off → on for pulse_ms → off (momentary) |
pulse | pulse | bool | — | toggle: on → off; off → on |
Pulse channels also report on in their state — the powered truth, read-only. Only the capability field (and pulse_ms) may be commanded on them; a running pulse is never extended; when a command carries no pulse_ms the board's default for that channel applies.
{ "channel": 1, "set": { "on": true } } → state { "on": true }
{ "channel": 2, "set": { "level": 65 } } → state { …, "level": 65 }
{ "channel": 2, "set": { "temp": 3000 } } → state { …, "temp": 3000 }
{ "channel": 3, "set": { "rgb": { "r": 255, "g": 120, "b": 0, "w": 40 } } }
{ "channel": 4, "set": { "source": "in_2" } } → state { "source": "in_2" }
{ "channel": 7, "set": { "position": "top_left" } } → state { "source": "in_2", "position": "top_left" }
{ "channel": 5, "set": { "pulse_on": true, "pulse_ms": 5000 } } → state { "on": false, "pulse_on": true } … { "on": true, "pulse_on": false }
{ "channel": 6, "set": { "pulse_off": true } } → state { "on": true, "pulse_off": true } … { "on": false, "pulse_off": false }Common shapes (illustrations, not identifiers)
| Looks like | Typical capability set |
|---|---|
| Plain switch / socket | [on_off] |
| Dimmer | [on_off, level] |
| Tunable white light | [on_off, level, color_temp] |
| Colour light | [on_off, level, color_rgb] |
| Input selector | [source_select] |
| Picture-in-picture inset | [source_select, position_select] |
Nothing on the wire names these shapes and new combinations appear without notice. Always drive from discovery.
Send a command
POST/v1/devices/{device_id}/commandsscope: command
{
"cmd_id": "booking-8841-start",
"channel": 2,
"set": { "on": true, "level": 60 }
}- A command does exactly what its fields say, nothing more.
{"level": 50}sets level and does not touchon. "On and dimmed" is{"on": true, "level": 50}: one command, both fields, atomic. No field ever implies another. - Any mix of fields from the channel's capabilities; at least one.
cmd_idis required and is your idempotency key (1–80 characters). It is keyed per API key: two of your keys never collide, and Nova never makes one up (a request without it is400 validation_failed). Generate it before sending and reuse it on every retry of the same intent.- Retrying with the same
cmd_idis always safe. A finished command replays its stored answer; a command still running is joined. Never executed twice. - Treat
409 board_offlineand504 timeoutas retryable with the samecmd_id; treat422as final. - Nova waits 8 s for the device. Set your HTTP client's timeout to at least 10 s, so the device's own answer (including its own
timeout) always reaches you before your client gives up.
The response is the device's actual result, not an acknowledgement
HTTP 200
{
"cmd_id": "booking-8841-start",
"device_id": "4f1d580f-…",
"channel": 2,
"status": "executed",
"state": { "on": true, "level": 60 },
"seq": 4294967342,
"error": null, "message": null,
"executed_at": "2026-10-10T07:41:30Z",
"replayed": false
}seq is the channel's sequence after the command: the same seq the state.changed webhook for this change carries, so you can drop that echo as already seen. It is null when the device rejected the command.
| Status | code / status | What happened | What to do |
|---|---|---|---|
| 200 | executed | The device applied it; state is the channel's resulting state | Done |
| 200 | replayed: true | Same cmd_id inside the 24 h window: the stored result, nothing re-executed | Treat as the original answer |
| 422 | bad_channel | No such channel on this device | Re-discover channels |
| 422 | bad_capability | Empty set, or a field the channel's capabilities do not carry | Build from discovery |
| 422 | bad_value | A field has the wrong type | Fix the payload |
| 422 | out_of_range | A value is outside the channel's constraints | Clamp to the constraints |
| 422 | status: rejected | The device refused it for a reason that will not change on a retry: bad_*, out_of_range, unauthorized, hidden, locked (error + message carry it). Stored and replayed on the same cmd_id | Final: the device is the authority; fix the command or re-discover |
| 409 | device_offline | The device is not connected. Nothing is queued | Retry when online (device.online webhook) |
| 409 | {"code": "board_offline", "channel"} | The box is connected but the board behind this channel is offline or quarantined, or the box's supervisor is unreachable. Transient: nothing queued, nothing stored | Retry with the same cmd_id: it runs again |
| 409 | device_blocked | Blocked by Nova | Owner contacts support |
| 504 | {"status": "timeout", "cmd_id", "channel"} | The board did not answer the box within 5 s. Not stored, like the cloud's own timeout below | Retry with the same cmd_id: it runs again |
| 504 | timeout | No answer within 8 s. The command stays pending: the device's late answer is stored and arrives as a state.changed webhook | Retry with the same cmd_id (it is safe), or wait for state.changed |
| 400 | validation_failed | Missing cmd_id / channel / set | Fix the payload |
Scenes: several channels in one frame
A scene is a preset the owner saved in the Nova app: a named set of channel targets on one device (“Recording” = lights 40 %, fan off). Partners cannot create or edit scenes, but a command key can apply one. Send scene instead of channel + set; the cloud expands it and sends the device a single atomic frame.
POST /v1/devices/{device_id}/commands
{ "cmd_id": "booking-8841-start", "scene": 3 }
HTTP 200
{
"cmd_id": "booking-8841-start", "device_id": "4f1d580f-…", "scene": 3,
"status": "executed",
"channels": { "1": { "on": true }, "2": { "on": true, "level": 40 } },
"errors": {},
"executed_at": "2026-10-10T07:41:30Z", "replayed": false
}statusisexecuted(every entry applied),partial(some entries refused by the device:errorsmaps channel → reason, still 200) orrejected(none applied, 422).- Scene numbers are per device and never reused: a deleted scene answers
404 scene_not_found, and a new scene gets a fresh number. Safe to key automation on. - Offline, timeout and idempotency behave exactly as for a channel command. Sending both
sceneandchannelis400 command_shape.
Idempotency, precisely
Nova validates the request first (a bad one is a clean 422 before anything wakes the device). Then it records the command as pending under your key and cmd_id before it reaches the device. An executed answer, or a deterministic refusal (422 rejected), is stored for 24 hours; a retry inside the window returns it with replayed: true. A retry that arrives while the first request is still waiting joins it and gets the same answer. A 504 timeout leaves the command pending: Nova keeps listening, stores the device's late answer and sends it to you as a state.changed webhook; a retry on a pending command re-sends it, and the device remembers recent cmd_ids, so it never runs twice. Offline, validation, board_offline and timeout answers are not stored, so a retry re-attempts.
409 device_offline and online reads false. The validation, idempotency and error contract above are final and testable today.// Node 20+: send with retry on timeout, idempotent by cmd_id
async function command(deviceId, channel, set, cmdId) {
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch(`https://api.novacube.io/v1/devices/${deviceId}/commands`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.NOVA_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ cmd_id: cmdId, channel, set }),
signal: AbortSignal.timeout(10_000), // at least 10 s: Nova waits 8 s for the device
});
const body = await res.json();
// retryable with the same cmd_id: 504 timeout and 409 board_offline; 422 is final
if (res.status !== 504 && !(res.status === 409 && body.code === "board_offline")) return body;
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
}
throw new Error("device did not answer");
}Java SDK
// The SDK retries a 504 (and a retryable 409) with the same cmd_id.
// Derive the cmd_id from your own key so a retry after a restart is the same command
// (omit it to have one generated).
CommandResponse r = nova.commands().send(deviceId, 2,
Map.of("on", true, "level", 60),
CmdIds.of("booking", 8841, "start")); // "booking-8841-start"
// a device refusal (422, status: rejected) is a result, not an exception
if (ResultStatus.isRejected(r.getStatus())) {
log.warn("refused: {} {}", r.getError(), r.getMessage());
}
boolean replayed = Boolean.TRUE.equals(r.getReplayed());
// scenes: executed | partial | rejected
SceneApplyResponse sr = nova.commands().applyScene(deviceId, 3);
if (ResultStatus.isPartial(sr.getStatus())) {
sr.getErrors().forEach((ch, why) -> log.warn("channel {}: {}", ch, why));
}Read state
GET/v1/devices/{device_id}/statescope: read
Nova keeps a projection of every device: its last full status plus every change since. Each channel carries its own sequence number seq; the device's seq is the highest of them. The state endpoint returns that projection. It is the reconciliation snapshot: after any gap in your webhook stream, one call here per device puts you back in sync.
{
"device_id": "4f1d580f-…",
"online": true,
"seq": 4294967342,
"as_of": "2026-10-10T07:41:30Z",
"channels": {
"1": { "state": { "on": true }, "seq": 4294967301 },
"2": { "state": { "on": true, "level": 60, "temp": 3400 }, "seq": 4294967342 },
"3": { "state": { "on": false, "rgb": { "r": 0, "g": 0, "b": 0, "w": 0 } }, "seq": 4294967297 },
"4": { "state": { "source": "in_1" }, "seq": 4294967297 }
}
}Java SDK
StateResponse s = nova.devices().state(deviceId);
Long seq = s.getSeq(); // null before the device's first connection
// keys are the channel number as text; each entry is { state, seq }
ChannelState ch2 = s.getChannels().get("2");
Map<String, Object> fields = ch2.getState();
long ch2Seq = ch2.getSeq();- A channel's
seqonly grows. Keep the lastseqyou saw per channel: astate.changedfor that channel whosedata.seqis less than or equal to it is stale, so discard it. There is no ordering across channels. The numbers are large (they survive a box restart); store them as 64-bit integers. - Before the device's first connection
channelsis empty andseqis null;onlineis false. That is also what you see for a device that was reset at the factory. - Reads never touch the device: they are answered from the projection, online or not.
Streaming
A box with a video mixer can stream its program to an RTMP server. Nova never holds a stream key: you encrypt the key to the box's own streaming key, Nova carries a blob it cannot open, and only that box opens it. The box streams to one destination at a time.
1. Read the box's streaming key
GET/v1/devices/{device_id}/streamscope: read
{
"device_id": "4f1d580f-…", "supported": true,
"stream_key": { "kid": "b483abd0153b8fb0", "public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…",
"signature": "MEUCIQ…", "verified": true, "identity_public_key": "MFkw…", "reported_at": "…" },
"state": "idle", "destination_id": null, "space": null, "health": null, "bitrate_kbps": null, "cache_pct": null,
"partner": null
}public_key is a P-256 key (base64 SubjectPublicKeyInfo). kid = the first 16 hex characters of SHA-256 over it. signature is the box's identity key over nova-stream-key/v1\n<serial>\n<kid>\n<public_key>; verified means Nova checked it against the certificate it issued to the box. supported: false = the box has no video mixer (or has not reported its key yet).
2. Seal the stream key
sealed_key = v1.<ephemeral point>.<nonce>.<ciphertext>, each part base64url without padding: a fresh P-256 key pair; ECDH with the box key; HKDF-SHA256 with salt = ephemeral point ‖ box point (both uncompressed, 65 bytes) and info = nova-stream-key/v1|<kid> → 32 bytes; AES-256-GCM with a 12-byte random nonce and the same info as associated data.
// Node 20+
import crypto from "node:crypto";
function seal(publicKeyB64, kid, streamKey) {
const box = crypto.createPublicKey({ key: Buffer.from(publicKeyB64, "base64"), format: "der", type: "spki" });
const eph = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
// uncompressed point: 0x04 ‖ X ‖ Y
const point = (k) => { const j = k.export({ format: "jwk" }); return Buffer.concat([Buffer.from([4]), Buffer.from(j.x, "base64url"), Buffer.from(j.y, "base64url")]); };
const shared = crypto.diffieHellman({ privateKey: eph.privateKey, publicKey: box });
const info = Buffer.from(`nova-stream-key/v1|${kid}`);
const key = Buffer.from(crypto.hkdfSync("sha256", shared, Buffer.concat([point(eph.publicKey), point(box)]), info, 32));
const nonce = crypto.randomBytes(12);
const c = crypto.createCipheriv("aes-256-gcm", key, nonce); c.setAAD(info);
const ct = Buffer.concat([c.update(streamKey, "utf8"), c.final(), c.getAuthTag()]);
return ["v1", point(eph.publicKey).toString("base64url"), nonce.toString("base64url"), ct.toString("base64url")].join(".");
}In Java, StreamKeys.seal(publicKeyB64, kid, streamKey) does the same, and streaming().start calls it for you (next step).
3. Start and stop
POST/v1/devices/{device_id}/streamscope: command
{ "cmd_id": "booking-8841-live", "on": true, "url": "rtmp://live.twitch.tv/app",
"destination_id": "room-1", "kid": "b483abd0153b8fb0", "sealed_key": "v1.BDkb…", "request_id": "k-1f2e3d4c5b6a" }
HTTP 200
{ "cmd_id": "booking-8841-live", "device_id": "4f1d580f-…", "action": "stream.start", "status": "executed",
"result": { "streaming": { "state": "connecting", "service": "Studio channel" } }, "replayed": false }Stop: {"cmd_id": "…", "on": false}. Scope command; idempotent by cmd_id like every command; online-only. request_id answers a stream.start_requested webhook (below). Nothing Nova stores holds your key or the sealed blob.
Java SDK
// Reads GET …/stream, seals the key to the box and sends only the sealed blob.
// On stale_key it re-reads the box key and re-seals once.
StreamCommandResult r = nova.streaming().start(deviceId,
StreamStart.of("rtmp://live.twitch.tv/app", streamKey)
.destination("room-1")
.answering(requestId) // when answering stream.start_requested
.cmdId("booking-8841-live"));
nova.streaming().stop(deviceId);
// no streaming key on the box: StreamingUnavailableException, nothing sent4. Offer destinations on the box's kiosk
PUT/v1/devices/{device_id}/stream-destinationsscope: command
{ "destinations": [ { "id": "room-1", "name": "Studio channel", "platform": "twitch" } ],
"valid_until": "2026-10-21T18:00:00Z" }Names only — never a key or URL. Up to 10; id is yours (1–64 letters, digits, _ and -); platform is twitch, youtube, facebook, kick or custom; valid_until is required, at most 31 days ahead (e.g. the end of a booking) — then the list disappears from the box and from Nova by itself. DELETE clears it early. A tap on the kiosk reaches you as stream.start_requested {device_id, serial, destination_id, request_id}; answer with step 3 within about 15 s.
Java SDK
StreamDestinationInput room = new StreamDestinationInput()
.id("room-1").name("Studio channel").platform(StreamPlatform.TWITCH);
nova.streaming().putDestinations(deviceId, List.of(room),
bookingEnd); // Instant, at most 31 days ahead
nova.streaming().clearDestinations(deviceId); // DELETE: end early| Code | Meaning |
|---|---|
streaming_unavailable | 409: the box has no streaming key (no video mixer, or not reported yet) |
stale_key | 409: sealed to an earlier key — read GET …/stream again and re-seal. The box can also answer 422 error stale_key (its key changed meanwhile); that answer is not kept for idempotency, so a retry with the same cmd_id and a re-sealed key reaches the box |
bad_sealed_key | 400: not a v1 sealed key (or 422 from the box: it does not open) |
sealed_key_required | 400: a partner start always carries a sealed key |
bad_url | 400: url is not rtmp:// or rtmps:// with a host (and no query), at most 300 characters |
stream_busy | 422 from the box: already streaming; stop first |
bad_destinations · bad_valid_until | 400: list size, duplicate ids or expiry out of bounds |
Kiosk access
A box with touch screens (kiosks) can be locked with a PIN and can hide pages. You can do it per booking: set a fresh PIN, hand it to the guest, lock again when they leave. The box enforces it; Nova never stores the PIN.
GET/v1/devices/{device_id}/kioskscope: read
{ "device_id": "4f1d580f-…", "supported": true, "locked": true, "pin_set": true, "lockout_until": null,
"pages": ["instructions", "camera", "ambience", "setup", "studio.owner", "studio.partner"],
"visible": ["instructions", "camera", "ambience", "studio.partner"], "partner_visible": null, "state_reported_at": "…" }POST/v1/devices/{device_id}/kiosk/pinscope: command
{ "cmd_id": "booking-8841-pin", "pin": "482913" } → both kiosks lock at oncePOST/v1/devices/{device_id}/kiosk/lockscope: command
{ "cmd_id": "booking-8841-unlock", "locked": false } → unlock without the PIN (or lock: true)PUT/v1/devices/{device_id}/kiosk/visibilityscope: command
{ "visible": ["camera", "ambience", "studio.partner"] } → null = you restrict nothingJava SDK
// getLocked(), getPinSet(), getVisible(), getPartnerVisible()
KioskState k = nova.kiosk().get(deviceId);
nova.kiosk().setPin(deviceId, "482913"); // both kiosks lock at once
nova.kiosk().unlock(deviceId); // without the PIN; lock(deviceId) again
nova.kiosk().setVisibility(deviceId,
List.of(KioskPages.CAMERA, KioskPages.AMBIENCE, KioskPages.STUDIO_PARTNER));
nova.kiosk().clearVisibility(deviceId); // null: you restrict nothing- The kiosks show the owner's pages ∩ yours: you can hide more, never show a page the owner hid. The owner's own set and auto-relock are not shown to you.
- PIN: 4–6 digits (
bad_pin). Lock without a PIN set:409 no_pin. No kiosk on the box:409 kiosk_unavailable. Commands are online-only and idempotent bycmd_id, like every command. - The owner can always override with a new PIN. Five wrong PINs at the kiosk freeze the pad for 5 minutes.
- Webhooks:
kiosk.locked {device_id, serial, src, change},kiosk.unlocked,kiosk.lockout {device_id, serial, retry_s}.srcsays who:app:…,partner:…,kioskorauto.
Remote control (owner setting)
The owner decides, per device, who may control it from afar. It is the owner's setting in the Nova app (device › Settings › Remote control); a key can read it but never change it.
| control_mode | Owner app | Your keys |
|---|---|---|
APP_AND_INTEGRATIONS | controls | control (the default; every device starts here) |
INTEGRATIONS_ONLY | sees the box, does not control it | control — for a box your system runs, e.g. through bookings |
APP_ONLY | controls | refused: 409 integration_control_off |
- Read it as
control_modeonGET /v1/devicesandGET /v1/devices/{device_id}. A change arrives as the webhookdevice.control_mode {device_id, serial, control_mode, previous}, so your system knows why commands start failing or working again. - Covered (refused under
APP_ONLYwith409 integration_control_off): channel commands and scene applies (POST …/commands), stream start/stop (POST …/stream), kiosk lock/unlock and PIN, and your kiosk page set. Never covered: reads (state, channels, kiosk, stream), your stream destinations. Treat the code as final for that device until adevice.control_modeevent says otherwise; retrying does not help. - The setting exists only on accounts with developer access, and applies only while your keys can act at all: if the owner presses the panic button or developer access is revoked, every device behaves as
APP_AND_INTEGRATIONSfor the owner (your keys stop anyway). - Bluetooth at the box is never blocked: the owner, standing next to it, can always control it and set its Wi-Fi. What they changed is reported when the box is back online as
device.local_change(below).
Changes made at the box
{ "type": "device.local_change",
"data": { "device_id": "4f1d580f-…", "serial": "NVA-SB-54723-T", "report_id": "8c1e…", "src": "ble:<account id>", "at": 1760000000,
"actions": [ { "type": "channel.set", "channel": 2, "set": { "on": true, "level": 60 } },
{ "type": "channel.set_many", "channels": { "1": { "on": false }, "2": { "level": 40 } } },
{ "type": "wifi.set_config", "ssid": "Studio", "result": "success" },
{ "type": "wifi.clear" } ] } }- One event per report; Nova delivers a report once even if the box resends it.
channel.set_manyis a scene the owner applied over Bluetooth. A Wi-Fi action never carries a password. Ignore action types you do not know. atis the box's own clock when it recorded the change and can be off on a box that was offline without time sync: show it, do not order or trust on it. The resulting channel states also arrive asstate.changed.
Webhooks: receive changes
Webhooks are the only push channel: Nova POSTs signed JSON to one endpoint per account whenever something in the account's fleet changes. Polling is unnecessary; the state endpoint confirms.
Set up (in the Nova app)
- Account → Developer settings → Webhook → Register. Enter an HTTPS URL on a public host (private and internal addresses are refused, and re-checked at every delivery).
- Pick the event types you want (the filter can be changed later).
- The app shows the signing secret once (Copy / QR). Store it next to your API key; you verify every delivery with it.
- Tap Send test event. A real
test.pinggoes through the real pipeline and the app shows your endpoint's response code and latency.
The envelope
POST https://partner.example.com/nova/events
Content-Type: application/json
X-Nova-Signature: t=1760081490, v1=5d41402abc4b2a76b9719d911017c592…
{
"event_id": "evt_2f8c1a9d7e4b3c6a5d1e",
"ts": 1760081490,
"type": "state.changed",
"data": { "device_id": "4f1d580f-…", "serial": "NVA-SB-54723-T", "channel": 2, "state": { "on": true, "level": 60 }, "seq": 4294967342, "actor": "partner:k7m2x9pqab" }
}Event catalogue
| type | When | data |
|---|---|---|
state.changed | A channel's state changed (owner app, your command, a physical switch, a scene); also fired once for a command's result, even a late one | {device_id, serial, channel, state, seq, actor} |
device.online | The device connected to Nova | {device_id, serial, seq} |
device.offline | The device has been unreachable past the debounce window. last_seen_at is an ISO-8601 instant, or JSON null if the device never connected | {device_id, serial, seq, last_seen_at} |
fault.raised | The device reported a fault | {device_id, serial, seq, event, fault, channel?, …} |
fault.cleared | The fault cleared | {device_id, serial, seq, event, fault, channel?, …} |
device.access_granted | A device entered the account's fleet: your keys can now reach it | {device_id, serial, via} |
device.access_revoked | A device left the fleet (released, transferred away, support action): your keys can no longer reach it | {device_id, serial, reason} |
test.ping | The owner tapped Send test event | {message} |
stream.start_requested | Someone tapped one of your destinations on the box's kiosk: answer with POST …/stream | {device_id, serial, destination_id, request_id} |
stream.started | The box is live | {device_id, serial, destination_id, space} |
stream.stopped | The stream ended as asked | {device_id, serial, destination_id, space, duration_s} |
stream.failed | The stream ended by itself | {device_id, serial, destination_id, space, reason} |
stream.degraded | The uplink is falling behind (the box's send buffer is filling) | {device_id, serial, destination_id, space, bitrate_kbps, cache_pct} |
kiosk.locked | The box's kiosks locked (a PIN set, a lock from the app, a partner, the kiosk or auto-relock) | {device_id, serial, src, change} |
kiosk.unlocked | The kiosks unlocked (the PIN at a kiosk, the app or a partner) | {device_id, serial, src, change} |
kiosk.lockout | Five wrong PINs: the pad is frozen | {device_id, serial, retry_s} |
device.control_mode | The owner changed the device's Remote control setting (under APP_ONLY your commands answer 409 integration_control_off) | {device_id, serial, control_mode, previous} |
device.local_change | The owner changed something at the box over Bluetooth; reported when the box was back online | {device_id, serial, report_id, src, at, actions} |
Every device event carries serial beside device_id (key your records by device_id; the serial is for humans). In fault.* events device_id, serial and seq come from Nova and the box's own event fields pass through as it reported them (minus its seq and ts): at least event and fault, channel when the fault is on one, possibly more — ignore what you do not know.
Who changed it: actor and seq in state.changed
data.seq is the channel's sequence (the same number the command response and GET …/state carry for that channel). data.actor says who caused the change, as a kind only; it never carries an account id.
| actor | Meaning |
|---|---|
app | The owner, from the Nova app through Nova |
partner:<key_id> | An API key. When the key_id is yours, this is the echo of your own command |
kiosk | Someone at the box's own screen |
ble | The owner at the box, over Bluetooth from the app |
support | Nova support |
box | The box itself: a physical input, a restore after power loss, a correction from a board |
state.changed carrying your own actor. Filter your own echo by actor = partner:<your key_id> (or by the seq your command response returned). Two integrations that each answer the other's changes would otherwise loop.device.local_change is unchanged: it still reports, once the box is back online, what the owner changed at the box over Bluetooth while it was offline.
There is deliberately no ownership or claim-history event: ownership facts are private. The access pair tells you only that a device is, or is no longer, reachable through your keys.
device.access_granted, device.access_revoked and test.ping are live today. The device-originated events (state.changed, device.online / offline, fault.*) are emitted by the device link and start flowing when devices connect through it; their shapes above are the contract.Verify the signature (always)
X-Nova-Signature carries a timestamp and one or more HMAC-SHA256 values over <ts> + "." + <raw body> with your signing secret, hex encoded. Reject anything whose timestamp is more than 5 minutes off, or whose signature does not match. Compare in constant time. Use the raw request body, not a re-serialised one.
// Node (Express): verify before parsing
import crypto from "node:crypto";
export function verifyNova(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
// during a secret rotation the header carries two v1= values; any match is valid
return header.split(",").some((p) => {
const [k, v] = p.trim().split("=");
return k === "v1" && v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected));
});
}# Python
import hmac, hashlib, time
def verify_nova(raw_body: bytes, header: str, secret: str) -> bool:
parts = [p.strip().split("=", 1) for p in header.split(",")]
ts = next(int(v) for k, v in parts if k == "t")
if abs(time.time() - ts) > 300:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(k == "v1" and hmac.compare_digest(v, expected) for k, v in parts)In Java, NovaWebhooks verifies the header (any v1 against any of your secrets, ±5 minutes) and parses the body into a typed NovaEvent: one record per event type, UnknownEvent for a type newer than your SDK. With the starter, inject the NovaWebhooks bean; without it, NovaWebhooks.of(secret, previousSecret).
@PostMapping("/nova/events")
ResponseEntity<Void> onEvent(@RequestBody byte[] body,
@RequestHeader(NovaWebhooks.HEADER) String signature) {
NovaEvent e;
try {
// the raw bytes, never a re-serialised body
e = hooks.verify(body, signature);
} catch (WebhookVerificationException bad) {
// bad.reason(): SIGNATURE_MISMATCH, TIMESTAMP_OUT_OF_TOLERANCE, …
return ResponseEntity.status(401).build();
}
if (!seen.add(e.eventId())) { // delivered at least once: dedupe
return ResponseEntity.ok().build();
}
if (e instanceof NovaEvent.StreamStartRequested r) {
StreamStart start = StreamStart.of(rtmpUrl, keyFor(r.destinationId()))
.destination(r.destinationId())
.answering(r.requestId());
queue.submit(() -> nova.streaming().start(r.deviceId(), start));
} else if (e instanceof NovaEvent.StateChanged s) {
queue.submit(() -> project(s.deviceId(), s.seq(), s.channel(), s.state()));
}
return ResponseEntity.ok().build(); // within 10 s; do the work after
}Secret rotation
Rotate secret in the app shows a new secret once. For 7 days every delivery is signed twice, once with each secret, so verification never breaks while you deploy. After the window only the new secret signs.
Delivery rules
- Answer 2xx fast (within 10 s), then process. Anything else, or a timeout, is a failure.
- At least once. Dedupe by
event_id. Forstate.changed, order per channel bydata.seq: drop one whoseseqis less than or equal to the last you saw for that channel (your last snapshot's channelseqincluded). There is no order across channels or across event types. - Retries after a failure: 1 min, 5 min, 30 min, 2 h, 8 h. Exhausted → the event is dead-lettered and the endpoint is flagged unhealthy in the owner's app (counts and types, never payloads).
- Paused when: you answer
410 Gone(immediately); the URL is gone (404, DNS failure, TLS mismatch) for one full retry schedule; or the server keeps failing (5xx / timeout / refused) for 24 h. The owner is notified and re-enables in the app. Missed events are not replayed: after re-enabling, reconcile withGET …/stateper device. test.pingnever affects retry, dead-letter or health accounting.
Webhooks notify, the API confirms. Treat the stream as hints; the state endpoint is the truth. A backend recovering from downtime reconciles with one state read per device.
Errors
Every error is an RFC 9457 problem document with Content-Type: application/problem+json and a stable machine-readable code. Switch on code, show detail to humans.
HTTP 422
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "level must be between 1 and 100",
"code": "out_of_range"
}| Status | Used for |
|---|---|
| 400 | Malformed JSON or missing required fields (validation_failed, with a fields map; malformed_body; cmd_id_required; command_shape; bad_parameter for a bad page/size). Streaming: bad_sealed_key, sealed_key_required, bad_url, bad_destination, bad_destinations, bad_valid_until. Kiosk: bad_pin, bad_page |
| 401 | Key problems: api_key_required, api_key_invalid, api_key_revoked, api_key_expired |
| 403 | insufficient_scope, partner_suspended, developer_access_revoked |
| 404 | device_not_found: not in your account, does not exist, or the id is not a UUID (indistinguishable by design) — the same code on every /v1/devices route; scene_not_found |
| 409 | device_offline, device_blocked, cmd_id_reused (the cmd_id was used for another device or kind of command); streaming: streaming_unavailable, stale_key, bad_stream_key; kiosk: kiosk_unavailable, no_pin; integration_control_off (the owner set the device to App only — see Remote control); retry (with retryable: true — another change was in flight, call again) |
| 422 | bad_channel, bad_capability, bad_value, out_of_range, or status: rejected from the device (its error field says why; a box stale_key is not kept for idempotency, every other rejection replays for the same cmd_id) |
| 429 | rate_limited, with Retry-After in seconds |
| 504 | timeout: the device did not answer within the bounded wait |
Java SDK
Every problem becomes a typed exception (all unchecked, under NovaApiException) carrying status(), code(), detail(), fields() and cmdId(). Branch on code().
| Status | Exception |
|---|---|
| 400 | ValidationException |
| 401 | AuthenticationException |
| 403 | PermissionDeniedException |
| 404 | NotFoundException |
| 409 | DeviceOfflineException (device_offline), StaleKeyException (stale_key), ConflictException (any other 409) |
| 422 | UnprocessableException (Nova's own checks); a device's status: rejected is a result, not an exception |
| 429 | RateLimitedException with retryAfter() |
| 504 | DeviceTimeoutException |
| none | NovaTransportException: no HTTP answer after every retry |
try {
nova.commands().send(deviceId, 2, Map.of("level", 60), cmdId);
} catch (DeviceOfflineException offline) { // nothing was queued
waitForOnline(deviceId); // retry on device.online
} catch (UnprocessableException bad) { // out_of_range, bad_capability, …
log.warn("{}: {}", bad.code(), bad.detail());
} catch (DeviceTimeoutException | NovaTransportException noAnswer) {
retryLater(cmdId); // same cmd_id: never executes twice
}Retries. RetryPolicy.defaults(): 3 attempts, backoff with full jitter from 0.5 s to 5 s; it retries a 504, a 409 with retryable: true and I/O errors, always re-sending the same body (the same cmd_id); on 429 it waits Retry-After when that is 60 s or less. Nothing else is retried. Change it with RetryPolicy.builder().maxAttempts(5).build(), or RetryPolicy.none().
Limits, versioning and contract rules
| Limit | Value |
|---|---|
| Requests per key | 120 per minute, fixed window; 429 + Retry-After past it |
| Command wait | 8 s for the device's answer, then 504 timeout (the command stays pending) |
| Your HTTP client timeout | at least 10 s on command calls |
| Idempotency window | 24 h per cmd_id, keyed per API key |
| Webhook timeout | 10 s for your 2xx |
| Signature tolerance | ±5 minutes on the timestamp |
| Page size | 25 default, 100 maximum |
The API is path-versioned (/v1/). Within a version changes are additive only: new fields, new event types, new capabilities, new kinds. Your code must ignore keys it does not know.
Five rules to build on
- Channel numbers are immutable: safe to persist, safe to key automation on.
- Ignore unknown keys in payloads and unknown kind values; the platform grows additively.
- Owner presentation (names, icons) is never exposed; maintain your own labels.
- Webhooks notify, the API confirms: reconcile with GET …/state, never trust the event stream as the source of truth.
- Build requests from discovery, not from product assumptions; the same integration must drive any device, current or future.
Changelog
| Date | Change |
|---|---|
| 2026-10-25 | Commands and state: cmd_id is required (400 validation_failed without it) and keyed per API key; a retry joins a running command; the command wait is 8 s (client timeout at least 10 s) and a 504 timeout leaves the command pending, its late answer arriving as state.changed; the 200 body carries the channel's seq; GET …/state channels are { state, seq }; state.changed data carries actor and a per-channel seq; transient box errors are no longer 422 rejected: 409 board_offline (board offline, quarantined or supervisor unreachable) and 504 timeout with channel (the board did not answer the box in 5 s), both retryable and never stored; 422 rejected only for deterministic refusals, stored and replayed. |
| 2026-10-20 | Contract fixes (additive): serial on state.changed, device.online, device.offline and fault.* data; device.offline last_seen_at is JSON null (was the string "null") for a device never seen; a malformed device id answers 404 device_not_found on every /v1/devices route; a box stale_key refusal is not kept for idempotency (retry with the same cmd_id re-attempts); stream url at most 300 characters (was 400 at the edge, 300 in the rules). |
| 2026-10-20 | Kiosk access (additive): GET …/kiosk, POST …/kiosk/pin and …/kiosk/lock, PUT …/kiosk/visibility, three kiosk.* webhook events. |
| 2026-10-20 | Streaming (additive): GET/POST …/stream with keys sealed to the box, PUT/DELETE …/stream-destinations, five stream.* webhook events. |
| 2026-10-10 | v1: keys and scopes, devices, channel discovery, capability catalogue, commands with idempotency, scenes (apply by number), state, webhooks with the v1 event catalogue and test.ping. |
The console at a glance
The staff console (/ after sign-in) is where Nova staff look things up and, now and then, act: who owns a device, whether a webhook is healthy, which firmware a board revision should be flashed with. Device owners never use it; they use the mobile app. Partners never use it; they use the API described above.
Who may use it
| Role | Can see | Can do |
|---|---|---|
| ADMIN | Every page, including Users (staff accounts). | Everything: create and manage staff, edit the catalogues, import devices, revoke, unbind, approve developer access. |
| SUPPORT | Every page except Users. The Users entry is not in the sidebar and the page redirects home. | Look up, read and copy. Verbs that change catalogue or device data are not shown at all (a verb the role may never use is hidden). |
The role decides what you may do in this console. Two areas are gated by permissions instead, granted per person by an admin (Users › person › Permissions): what you may do on a station (download firmware, flash boards) and what you may do in the Inventory (see it, receive lots, record tests, change a board’s condition). A new SUPPORT account has none; an ADMIN holds every permission.
A verb that is shown but greyed out is a different thing: the role may use it, but the current state forbids it. The reason is written next to it (in the Actions menu, under the item; beside a header button as a short line), never hidden in a tooltip — for example “The last active admin: add another admin first.”
How the screen is laid out
- Top bar: the Nova Core brand (click it to go Home), the words “Internal console”, your name and role, and Sign out.
- Left sidebar: the primary navigation in four sections — Accounts (Users, admins only · App users), IoT core (Products · Boards · Devices · Stations), ERP (Stock · Lots · Boards · Parts · Devices · Models — the Inventory, with the
inventory.readpermission; the section is absent without it) and You (Account). Boards and Devices appear in both IoT core and ERP on purpose: the IoT core's are the catalogue definitions and the cloud registry, the ERP's are the physical units in stock. On a phone the same list opens as a drawer behind the menu button in the top-left corner. - Page header: a breadcrumb (on object pages), the title with its status badges, a line of key facts (identity, who and when; retired or revoked reasons), and on the right one primary verb — the next step for that object — plus an Actions menu holding every other verb. Objects that walk a fixed sequence (an assembled unit, a config version) draw it as a row of steps under the header; the current step is the primary verb.
- Tabs under the header on object pages: overview first, then the tabs that hold a long list or live data, logs last. On an object page each tab shows its count; the tabs of a list page (Products, Boards, Devices, Stations) carry none. The chosen tab is part of the address bar (
?tab=…), so a copied link opens the same tab. Most objects have only overview and logs: what you came for is on the overview, stacked.
Things that work the same everywhere
- The address bar is the state. Filters, sort order, page number, the open tab and even an open dialog (
?action=…) are in the URL. Paste a link to a colleague and they land on the exact same view. The browser’s Back button closes a dialog. - Lists are paged. Every long list shows “x–y of total” at the bottom with Previous / Next and a rows-per-page choice (10, 25, 50, 75, 100). Changing a filter returns you to page 1. Nothing is filtered or sorted in the browser; the server answers exactly the page you asked for.
- Search boxes wait about a third of a second after you stop typing before searching. Sort by clicking a column header; click again to reverse.
- Filter rows are the same everywhere: the search box first, then the filters in order of how much they narrow, each at a fixed width (a select is never as wide as its longest option; option labels are a code and a short name), then Clear filters, which appears once any filter is set — in the toolbar when there are results, in the empty state when there are none. A filter that depends on another (the config version depends on the product model) is a select fed by that choice and greyed out until it is made. A default that is not “all” shows in the control (Lots start on Open). Short catalogues (product lines, board roles) are listed whole with no filter row; anything that can grow is paged with one. On a phone the filters fold behind a Filters (n) button and wide tables turn into stacked cards, each cell labelled with its column.
- Addresses name the object, never its id: a serial (
/devices/NVA-TTB-00002-X,/inventory/boards/NVA-PB-00007-2), a lot code (/inventory/lots/L-00047), a catalogue code (/products/models/SB/M1,/boards/revisions/PB/M1/RevE) or an item code. Type one straight into the address bar, or scan a sticker into it. People, app users, stations and transfers have no code, so they keep their id. An old id address still opens the page and moves to the code. - Links stay in this tab inside a sidebar section and open a new tab across sections. A role → model → revision link in IoT core, or a lot → board link in ERP, replaces the page; an owner named on a device (Accounts), a catalogue revision named on an ERP board, or an ERP unit named on a registry device opens beside you. A link always goes to the object’s own page, never to a filtered list. The breadcrumb takes you up; there are no “Find in list” links.
- Times are shown relative (“3 hours ago”); hover to see the exact UTC time.
- Ids, emails, codes and keys have a copy button. Long ids are shortened to 8 characters with the full value on hover.
- Comments. Every object page's Logs tab has a Comments view beside Activity: type an update and Post comment. Any staff role may post; a comment shows the author's name and time, newest first, paged. You may edit or delete your own comments (Edit / Delete beside the time; an edited one is marked “edited”); everyone else's are read-only. Comments are separate from the activity log, which only the system writes — a deletion leaves an audit entry there.
- Values edited in place. On every overview, a field you may change shows a pencil at the right edge of its row (name, description, notes, a status, the permissions list…). Click it, change the value, then the tick saves and the cross cancels; Escape is the cross, Enter saves a single-line value. A cross on unsaved changes asks “Discard unsaved changes?” first. Nothing opens a separate form any more.
- Dialogs close with Escape, the × button or a click outside. If you typed something, the dialog first asks “Discard unsaved changes?”. Leaving a page with unsaved edits asks for confirmation too.
- Errors: a problem with one field is shown under that field. Any other problem appears as a red banner with a short error code and a copy button; quote that code when you ask for help.
- A verb that ends in “…” opens a dialog or an editor; one without it acts or navigates at once. Every change that cannot be undone, that an owner will notice, or that the audit log records with a reason goes through a dialog that needs a Reason of at least 3 characters (the reason goes into the audit log, and on the app-user pages the owner sees it too). Only the confirm button of an irreversible step is red — Delete, Scrap, Publish, Release, Withdraw, Revoke a key, Unbind; a reversible one (Retire…, Disable…, Revoke access) is not.
- Every action is audited. Each object page has a Logs tab with two views (
?log=): Activity — the audit log (or the Inventory's custody ledger), newest first, paged, who did what, the reason, and a collapsible “Details” block with the raw record — and Comments, the staff notes on that object. - Status badges read as words, never as codes: Active / Retired for catalogue objects (retired = no new children, existing ones untouched, reversible with Restore); Draft / Published / Retired for product models; Draft / Released / Withdrawn for firmware releases; Current marks the one release or config version new units are built to; Profile fixed / Channels fixed means a table can no longer change because something downstream now depends on it. Four colours, one meaning each: green = good or finished well (released, OK, online, delivered, activated); amber = a person must act (requested, failing, unresolved, dead on arrival, EOL failed, RMA); red = blocked or failed (revoked, faulty, fail, disabled); grey = neutral, pending or informational (draft, pending, retired, withdrawn, scrapped, a category). Hover a badge for its one-line meaning.
Where each page sits in the build flow
The pages are used roughly in this order when a new product goes from an idea to a box in an owner’s home. Each step names the page and the person who normally does it.
- Board role (Boards › New role, ADMIN) — once per kind of board (power, LED, display, master). Pick its check mode while it has no models.
- Board model (role page › Models › New model) — one per hardware generation. Then author its port profile on the Profile tab: one port per load, its capabilities, physical limits and the schematic load letter. Do this before any firmware is published, because publishing freezes the profile.
- Board revision (board model page › Revisions › New revision) — created after the hardware team has a tested PCB and a lineage letter (RevC, RevE). Type the letter if the silkscreen carries one.
- Firmware release (revision page › Releases) — the engineer builds the image from a tagged commit, uploads it (draft), publishes it, and makes it current. The factory flashes only the current released firmware (see the Station agent book).
- Station (Stations › New station, ADMIN) — once per machine that flashes boards. Copy the key (shown once) into the agent on that machine; from then on an operator who has the station permissions (Users › person › Permissions) runs
nova-station login, approves the code on Station sign-in, and flashes. Every flash appears on the station’s Activity tab under that person. - Receive the boards (ERP › Lots › Receive lot,
inventory.receive) — when a shipment of a board revision arrives, book it as a lot: model, supplier, PO, quantity. Without an open lot a station cannot record a new board of that revision. - Flash and record (the station, then ERP › Boards) — the operator scans the board’s sticker (printed from the lot), the agent flashes and reports serial plus chip id; the Inventory binds the chip to the serial at the first flash. The board’s end-of-line result and any condition change (faulty, RMA, scrapped) are recorded on its Inventory page; Stock shows what is ready to assemble.
- Product line and product model (Products › New line; line page › Models › New model) — what Nova sells. Author the model’s channel table (the controls an owner will see) while it is a draft, then Publish it. The channel numbers are a promise to owners and partners and never change after that.
- Config version (product model page › Configs › New config version…) — says which board models deliver which channel; the builder is its own page, seeded from an earlier version if you like. Needs the board models from steps 2–3. Each version has its own page: complete its pack (documents, BOM, seed), Release… it, then Make current… so the factory builds to it.
- Assemble (ERP › Devices › Assemble,
inventory.assemble) — scan the stickers of tested boards, the Pi, parts and consumables into one unit with its serial. The unit’s page then walks its sequence — Verify build… › Install identity… (at the station: the Pi’s certificate issued, installed, activated, and the unit’s device map installed) › Pre-register… (sends it to the IoT core with its activated identity and label pair, shown once) › Record device EOL… › Ship… — one step at a time as the primary button. Devices are born only this way: there is no New or Import verb in the registry. - Claim — the owner scans the label in the mobile app. The console shows the binding on the device page and on the owner’s App user page; the first owner claim activates the unit and sends a fact to Inventory (Devices › Events).
- Support — App users, Devices and their Activity tabs are the daily lookup pages; unbind, revoke, new label and developer-access decisions are the ADMIN actions.
Sign in, forgot password, expired session
Sign in — /login
What: the staff sign-in form: Email, Password, a Forgot your password? link and a Sign in button. Who: any staff account (ADMIN or SUPPORT). Owners cannot sign in here.
- Opening any console address without a session sends you to
/login?next=…. After signing in you land on the exact page you asked for, including its tab and filters. The subtitle says “You will be taken back to where you were.” when that applies. - A wrong email or password shows “Email or password is incorrect.” Any other failure shows “Sign-in failed. Try again.”
- If an admin created your account or reset your password, you sign in with the temporary password and are taken to Account to choose your own; nothing else works until you do.
- Signing in on a second browser signs the first one out: one active session per staff account.
Expired session — /login?reason=expired
When a session has run out (idle or absolute limit, or your password was reset elsewhere), the next click lands on the sign-in page with the notice “Your session has expired. Sign in to continue where you left off.” Sign in and you return to the page you were on.
Forgot password — /forgot (two steps)
- Step 1 of 2 — Reset your password. Enter your staff email and press Send code. The page always says “If an account exists for <email>, we've sent a 6-digit code. It expires in 10 minutes.”, whether or not the email exists. That is on purpose: the page never reveals which emails are staff accounts. If no code arrives, check the address, or ask an admin whether your account still exists. Test mode only: on a local or test backend (every code is
111111, no email is sent) a yellow “Test mode only” box says when no code was issued, because no staff account uses that email or the account is disabled. Production never shows it. - Step 2 of 2 — Enter the code. Type the 6 digits (only digits are accepted), then the new password twice. The checklist under the password turns green rule by rule. Set new password stays disabled until the code has 6 digits, every rule passes and both passwords match.
- Resend code is available again 30 seconds after the last send (“Resend in 12s” counts down). Resending clears the code box.
- “That code is wrong or has expired. Check the email address, or ask an admin if you no longer have an account.” means try again, resend, or check the address you typed. After 3 wrong codes you see “Too many wrong codes. Request a new one.” and are sent back to step 1.
- Success shows “Password changed. Every previous session was signed out.” and a Back to sign in button.
The step is part of the address (?step=code&challenge=…), so a refresh resumes at the code screen; the code itself is never in the URL.
Password rules (everywhere a password is set)
| Rule |
|---|
| At least 12 characters |
| An uppercase letter |
| A lowercase letter |
| A number |
| A symbol, such as ! @ # - _ |
| No spaces at the start or end |
| At most 128 characters |
Home
What: a greeting — “Hello, <your name>” with your role badge and the line “Pick a section from the navigation. Every action in this console is audited.” Nothing else, by rule: Home is never a dashboard and never gets shortcut tiles. New features get a sidebar entry and their own pages.
Where: /; the brand link in the top bar brings you back here. Who: every staff role. When: right after sign-in; from here you choose a section in the sidebar.
Account
What: your own staff profile and the place to change your password. Where: /account, sidebar You › Account, the last entry. Who: every staff role, for their own account only.
Profile card
Name, email (with copy) and role. These are read-only here: “Profile details are managed by an admin.”
Permissions card
Read-only: what you may do beyond your role — on a station (each scope with what it allows, where that station allows it too) and in the Inventory. With none it says “No permissions — you cannot sign in on any station or work in the Inventory. Ask an admin to grant them.” An admin changes them on your user page (Users › you › Permissions).
Change password card
- Type your Current password.
- Type the New password; the rule checklist turns green as you go. Type it again under Confirm new password; the field says “Passwords match.” or “Passwords do not match.”
- Press Change password (enabled only when every rule passes and both fields match). A green “Password changed.” confirms it. “Choosing a new password signs out every other session.”
Forced change (/account?reset=1): after an admin created your account or reset your password, the console sends you here with the notice “Set a new password to finish signing in. Other sessions will be signed out.” Every other page shows “You must change your password before continuing.” until you do; once done you are taken to the page you originally asked for.
If the form has text in it and you try to leave, the browser asks you to confirm (“Unsaved changes” is shown next to the button).
Users (staff accounts)
What: the staff accounts of this console, titled “Internal users”. Where: /users, sidebar Accounts › Users, the first entry. Who: ADMIN only. SUPPORT does not see the entry and is redirected home if they type the address; the backend refuses them too. When: when someone joins or leaves Nova, changes team, or is locked out.
The list
- Search by name or email; filter by status (Active / Disabled). Sort by User, Status, Role or Last login.
- Columns: User (name links to the user page; email with copy; a “you” badge on your own row), Status (Active green / Disabled red, plus an amber “password reset pending” when the person still has to change a temporary password), Role (Admin / Support), Last login, and the row verbs Reset password… and Disable… / Enable… (the same dialogs as on the user page, below). A role is changed on the user page.
- You never see verbs on your own row.
New user… (primary button, ?action=create)
- Display name (up to 120 characters) and Email.
- Role: SUPPORT by default (“Support can do everything except manage staff accounts.”) or ADMIN (“Admins can create, disable and reset other staff accounts.”).
- Temporary password, shown in clear so you can pass it on; it must satisfy the password rules.
- Create user. “They sign in with a temporary password and must change it at first sign-in.” An already-used email is reported under the Email field (“email_taken”).
User page — /users/<id>
Breadcrumb Users › name; title with badges (you / Active or Disabled / Admin or Support / password reset pending); email and last login underneath. On the right, Reset password… (the primary verb) and the Actions menu with Disable… / Enable… and Change role…. On your own page the verbs are greyed out with “You cannot change your own account; ask another admin.” (change your password under Account); on the last active admin, Disable and Change role say “The last active admin: add another admin first.” Tabs: profile (Profile card: Id, Role with what it allows, Created, Updated; Permissions card, below) and logs (the audit log for this account: sign-ins, changes made to it and by it, “Changed station permissions” with what was added (+) and removed (−); other staff named in it link to their pages).
The dialogs (ADMIN)
- Reset password…: a visible Temporary password field with the rule checklist. “Sets a temporary password, signs them out everywhere, and forces a change at next sign-in. Share it with them directly.”
- Disable… (active accounts): a required Reason. “They are signed out everywhere at once and cannot sign in until re-enabled. The account and its audit history are kept.” Reversible, so the button is not red. Enable… (disabled accounts): a confirm — “They can sign in again with their existing password.”
- Change role…: pick Admin or Support (the hint says what the role allows) and confirm; the backend refuses demoting the last active admin.
Permissions (profile tab, ?action=edit-station-scopes)
What this person may do beyond their role: one row per permission with what it allows. Station permissions (for example board.flash — “Flash boards and record the result”) apply only where the station also allows them (see Stations › Permissions (scopes)); Inventory permissions (inventory.read, inventory.receive, inventory.test, inventory.assemble, inventory.rma) gate the Inventory section of this console. With none it says “No permissions — this person cannot sign in on any station or work in the Inventory.” SUPPORT accounts start with none; an ADMIN holds every permission whatever is ticked here.
- Click Edit permissions. A checkbox per scope in the catalogue, each with its description.
- Tick what the person needs; with nothing ticked the form warns that they cannot sign in on any station or work in the Inventory.
- Save (enabled once something changed; Cancel asks before discarding). The change applies at once — also to a session the person already has on a station: a removed scope is refused on their very next command.
Who: ADMIN only (the whole Users page is). Everyone sees their own permissions, read-only, on Account. When: someone starts or stops working at a station, or needs a new kind of station work.
App users (device owners)
What: the people who registered in the Nova mobile app — the device owners. “Read-only: open an owner to see sessions, activity, API keys and webhook.” Staff never create, edit or delete owners here. Where: /app-users. Who: every staff role can read; a few ADMIN-only actions live on the owner’s page. When: support calls (“I cannot sign in”, “my code says expired”, “my webhook stopped”), developer-access requests, and checking who owns a unit.
The list
- Search by name, email or id.
- Filters: status (Active / Disabled); verification (Verified / Unverified); developer access (Access requested / Granted / Rejected / Revoked / Not requested).
- Sort by Owner, Status, Registered (newest first by default) or Last login.
- Columns: Owner (name links to the page; email with copy), Status (Active / Disabled) with extra badges — amber “Unverified” when the email was never confirmed, amber “Access requested” when the owner asked for developer access and is waiting for you — Registered, Last login.
- Empty state when nobody matches, or “No one has registered yet. Owners appear here as soon as they register in the mobile app.”
App user page
Where: /app-users/<id>, opened from the list. Breadcrumb App users › name. The title carries the status (Active / Disabled), “Unverified” while the email is unconfirmed, and “Factory tester” when the account is an EOL-station account. Under it: email with copy, Registered, Last login. On the right the Actions menu (ADMIN): Mark as factory tester… / Remove factory-tester mark….
Eight tabs. The number on a tab is the count of rows; an amber dot on keys means the partner surface needs attention; the dot on webhook is its health (green healthy, amber failing, red paused).
| Tab | What it shows | Actions |
|---|---|---|
| profile | Id, Email verified (or “Not yet — cannot sign in or claim devices”), Factory tester (Yes / “No — a regular owner”). Second card “Current device”: the live phone session (device, signed in, last seen, expires) — one active sign-in per account; a new login on another phone evicts this one. | Actions menu (ADMIN): “Mark as factory tester…” / “Remove factory-tester mark…” (reason required). Use for EOL-station accounts only: every claim by such an account is recorded as a factory test — the unit is not activated and no warranty starts; the label proof pair survives the test. The bootstrap app account (the bootstrap admin email) needs no marking: the backend creates it at its first start already verified and marked factory tester (Logs: “bootstrap.app_user_created”, then the factory-tester mark by the system), and it is the account device EOL runs claim with unless the deployment names another one; an existing account with that email is never changed. |
| devices | Units currently bound to this account: Serial (link to the device page), Model, Name the owner gave, State (plus “Factory test” when applicable), Claimed. Ownership history is on each device’s Claims tab, not here. | none (read-only) |
| transfers | Offers this account sent or accepted, newest first: Created (opens the offer’s page), Direction (Sent / Received), Other party (link; “not yet accepted” while pending), Devices (serial links), Status with the cancel reason, Expires / closed. Filter by transfer status. | none |
| sessions | Every sign-in from the app, newest first: Device, State (Active / Evicted / Signed out / Password reset / Password changed / Account disabled / Expired, with when), Signed in, Last seen, Id. | none |
| notifications | What the owner was told, newest first: When, title + body + type, Tier (property & security / availability / integration), Device, Push outcome (pushed · push pending · push failed · not pushed · feed only), Email outcome (emailed · email pending · email failed · not emailed · no email), Read. The in-app feed is the durable store; push and email are only its delivery channels. Rows caused by the owner’s own action are feed-only by rule. | none |
| keys | The developer-access state and the owner’s API keys (see below). | ADMIN decisions on developer access; platform retirement or emergency revoke of a key. |
| webhook | The owner’s webhook endpoint and recent deliveries (see below). | none — “Registered by the owner in the app; staff cannot edit it.” |
| logs | The audit log of this account (registration, verification, claims, releases, transfers, staff actions with reasons) and the staff comments. | none |
Keys tab — developer access
Developer access is off by default and granted on request (ruled 2026-10-10). The banner at the top states the situation; the “Developer access” card shows the state and whether the owner has the Developer settings section switched on in the app (“Off — section hidden; keys still work”). The buttons depend on the state:
| State | Banner | Buttons (each opens a reason dialog; the owner sees your reason) |
|---|---|---|
| not requested | “No developer access. Off by default; the owner requests it under Account → Developer in the app.” | Grant developer access (tooltip: grants access without a request from the owner) |
| access requested | “Developer access requested” with when and the owner’s note. | Approve request… (owner gets the switch, keys and webhook; told by push and email) · Reject request… (owner can ask again after the cooling-off period, 30 days by default) |
| granted | none, or “Owner pressed the panic button” when the owner suspended all partner access themselves. | Revoke developer access…: suspends every key and the webhook, nothing is deleted, reversible with Restore |
| rejected | “Developer access rejected” with the reason and the date the owner may ask again. | Grant developer access |
| revoked | “Developer access revoked by Nova” with the reason. | Restore developer access: keys and the webhook resume working |
Keys tab — API keys
- Live keys are listed; revoked ones sit in a collapsed “N revoked keys” section. Each row: label, key id (copy), status badge (Active / Retiring, with its expiry / Revoked; “suspended” beside it when the owner or Nova suspended access), scopes, and facts: created, last used and from which address, “N requests in 24 h”, which key it replaces or is replaced by, whether a retirement was platform-initiated or an owner rotation.
- Usage toggles the key’s request log underneath (When, Request, From, Status), 10 rows a page; the open log and its page are in the URL (
?usage=…&upage=). - Retire (platform)… (active keys, when access is granted): a reason plus “Days until it expires” (1–90, default 7). Same retiring state as an owner rotation, marked platform-initiated; the owner is told and can rotate with one tap; the key keeps working until the date.
- Revoke now… (red, irreversible): for a confirmed compromise only. The key stops working at once; the owner is told afterwards. Prefer retirement unless the key is known to be leaked.
Webhook tab
“Webhook endpoint” card: URL (copy), State (Healthy; Failing since … with the number of consecutive failures; Paused with the date and why — the endpoint answered 410 Gone, could not be found, or kept failing for 24 h — “Only the owner can re-enable it (missed events are not replayed)”), Events subscribed, Signing secret (never shown; rotation dates), Registered. “Recent deliveries” card: Event (with a “test” badge for test pings, which do not count towards health), When, Status (Delivered / Pending with the retry time / Failed / Skipped), Response code and latency, Attempts. Paged.
Products: lines and models
What: the sales catalogue. “What we sell: lines group models; a model owns its channel table and its config versions.” Where: /products. Who: every staff role reads; ADMIN creates and edits. When: when a new product family or model is defined, before the factory can build units of it.
Tab “lines” (default)
Every product line (a family such as Studio Box or Pod Box): Code (links to the line page), Name and description, Models (the count links to the line’s page), Created.
New line… (ADMIN, primary button, ?action=new-line): type the Name (up to 60 characters); the Code is proposed from the initials as you type (“Studio Box” → SB) and can be overridden with 1–6 letters or digits; an optional Description. “The code becomes the prefix of every model under it (SB → SB/M1) and cannot change later.” Errors land under the field: a missing name, a bad code, a code already taken. On success you land on the new line’s page.
Tab “models” (?tab=models)
The one global list of product models, paged: search by code or name, filter by line and by state (Draft / Published / Retired), sort by Code, Name, State or Created. Columns: Code (SB/M1, links to the model page), Name, Line, State badge (hover for its meaning), Channels (count, links to the Channels tab), Current config (vN, links to the Configs tab, or “—”), Created. There is no “New model” here: models are added from a line’s page.
Product line page
Where: /products/lines/<CODE> (by code, e.g. /products/lines/SB). Breadcrumb Products › SB. Header: the line’s name, its code with copy, Created, Updated; Delete… (ADMIN, greyed out with “Lines with models cannot be deleted” while it has models). Tabs: overview · logs. Who: all staff read; ADMIN edits.
Overview
- “Line” card: Code, Name and Description (pencil: edited in place, ADMIN).
- “Models” card, right under it: the sellable models of this line — Code (→ model page), Name, State, Channels, Current config, Created. New model… (ADMIN): the dialog says “Will be SB/M2 — Model numbers are assigned in order and never reused. It starts as a draft so you can author its channel table.” Name is required (up to 60), Description optional. On success you land on the new model’s page.
- Delete…: a red-button dialog with a reason; “The audit history is kept.” You return to the Products list.
Logs
Created / updated / deleted events for the line, with the staff member and reason. SUPPORT sees staff as short ids because it cannot list staff accounts.
Product model page
Where: /products/models/SB/M1 (by code; an id address moves there). Breadcrumb Products › line › SB/M1. Header: name with the state badge (Draft grey / Published green / Retired grey; hover for the meaning), full code with copy, the line, “Current config vN” or “No current config”, the retire reason when retired. On the right: Publish… (drafts, the primary) and the Actions menu with Retire… / Restore… (published or retired) and Delete… (drafts, red). Tabs: overview · channels · configs · logs.
The three states
| State | Meaning | What you can do (ADMIN) |
|---|---|---|
| Draft | The channel table can still change. Publish it to fix the promise and start adding config versions. | Edit name/description · Edit channels… · Publish… · Delete… (only drafts can be deleted) |
| Published | The channel table is fixed. Config versions say which board models deliver it. | Edit name/description · New config version… · Retire… |
| Retired | No new config versions or units. Existing units keep working. | Restore… (reason) — the model is published again and accepts new config versions |
Header actions
- Publish… (drafts): “Publishing fixes the channel table for good — N channels — so owners, partners and scenes can rely on the numbers. After this you add config versions naming the board models that deliver it. A different channel table is a new model.” Disabled with “Add at least one channel first” while the table is empty.
- Retire… (published, reason; reversible, so not red): “No new config versions and no new units; existing units keep working. Reversible with Restore.”
Overview
“Model” card: Full code, Name and Description (pencils, ADMIN), Line, Channels (count → tab), Current config (→ that version’s page), Config versions (count → tab), Units (“Devices of SB/M1” opens the device registry filtered to this model in a new tab). “Timeline” card: Created, Published, Updated.
Channels
“What owners see and control: numbered channels (1, 2, … in order), each with a name, a kind and the capabilities it needs. Config versions bind board ports to these numbers. Fixed once published.” Table: No., Name, Kind, Capabilities (badges; hover for the long name), Narrowed limits (or “port limits” when the delivering port’s own limits apply), Default (the state the box restores the channel to, e.g. “on · level 60”, or —) and Owner may rename (Yes / No / — not stated). The card button reads Edit channels… on a draft and Channels fixed (disabled, with the reason on hover) afterwards. The editor itself is described in the next section.
Configs
The versions table only — each version opens its own page (see “Config versions and the builder”); New config version… opens the builder page.
Logs
Created, updated, channel-table changes, publish, retire/restore, config versions added or made current, documents uploaded, seed set, versions released.
Channel editor (product model, drafts only)
Where: product model page › Channels › Edit channels… (?tab=channels&action=edit-channels). Who: ADMIN, and only while the model is a Draft. When: right after creating a product model and before publishing it. Why it matters: the channel numbers are what the app renders and what partners address; after publishing they never change.
How
- Start from a board: the Add from model… drop-down lists every board model that has ports (“PB/M1 · Power board M1 (11 ports)”). Choosing one adds one channel per port, named and typed like the port, with no narrowing. Or press Add channel for an empty row.
- Per row: the number at the top left is its position (it renumbers when you move rows; it is text, not a field). Name (required, up to 32 characters, e.g. “Ceiling dimmer”). Kind from the vocabulary. ↑ / ↓ move the row, Duplicate copies it below, Remove deletes it — a row that holds something asks “Remove channel N?” first. A newly added row takes the keyboard focus.
- Capabilities: tick what the channel must be able to do (On / off, Level (dimming), Colour temperature, Colour (RGB), Source select, Position select (where the picture sits — e.g. the inset corner; its limit is the Positions list), Pulse on, Pulse off, Pulse (toggle)). Only board ports that have every ticked capability can deliver the channel later.
- Narrowed limits appear for capabilities that take limits. “Optional. Leave empty to use whatever the delivering port allows; a value here must lie inside the port’s limits.” Ranges are min–max pairs; both empty removes the limit.
- Save channels (the Save / Cancel bar stays visible at the bottom of the window while you scroll). The whole table is saved at once. A backend complaint about one row (“Channel 3: …”) is shown in red under that row with a red border; any other problem is a banner. Cancel asks before discarding when something changed.
- Default and naming (optional): one field per ticked capability for the state the box restores the channel to — On / off default (No default / On / Off), Level default (0–100), Colour temperature default (kelvin), Colour (RGB) default (
[r, g, b]or#rrggbb), Source / Position default (a select of the narrowed list when there is one, else typed), pulse defaults (Yes / No). Stored asdefaultStatekeyed by the command field (on,level,temp,rgb,source,position,pulse_on…); a field that belongs to none of the channel’s capabilities is refused withbad_defaulton that row. The tick The owner may rename this channel setseditable. Both become the seed’sdefaultandeditableand are fixed at publish with the rest of the table. - Two views: Form (rows) and JSON — the same table as text, in the shape of the bench file’s
console.channels(model.json, withdefaultStateandeditable), so a product folder’s channel table pastes straight in. Numbers are assigned by position whatever the JSON says; switching back parses the JSON and a shape error is shown under the box (ruled 2026-10-18, every structured editor has this toggle).
Config versions and the builder
Where: product model page › configs tab (?tab=configs) lists the versions; each version has its own page, /products/models/SB/M1/configs/<N> (the older ?tab=configs&version=N links redirect there). What: “Which board models deliver the channels, per version. Drafts are released once their pack is complete.” Who: all staff read; ADMIN adds versions, releases them and chooses the current one. When: after the model is Published and the board models exist; again whenever the factory should accept another board (e.g. “accept LB/M2 when TM1814 stock runs out”).
The versions table (configs tab)
- Columns: Version (vN opens the version’s page; a green Current badge marks the one new units are built to), State (Draft grey = the pack is being prepared, the assembly line does not see it; Released green = the line can build to it), Accepted models per role, Notes, Created when and by whom.
- New config version… (ADMIN) opens the builder page. On a draft model the card says instead “Publish the model first: a config version binds boards to a fixed channel table.”; on a retired one “The product is retired; restore it to add config versions.”
The version page
Breadcrumb Products › line › model › vN. Header: “Config vN · SB/M1” with the Current and state badges; the model (link), released and created when and by whom, the notes. Under it a three-step row — Draft › Released › Current — and the version picker: one chip per version (● = current) to jump between them. On the right: Release… (ADMIN, drafts; greyed out with “Every checklist item must pass first.” until it does) and the Actions menu: Make current… (released, not current; a draft says “Release the version first”), New version from this one… (the builder seeded with this version’s roles), Import BOM… / Re-import BOM… (inventory.receive, drafts; a released version says “Released: the BOM is frozen. A change of recipe is a new version.”), Export BOM csv.
- Release checklist (first): Product model published · One SELF-check role (exactly one accepted role with checkMode SELF — the gateway that runs the supervisor and carries the unit’s identity) · Firmware current (every accepted board model has an active revision with a current release built from the model’s current profile, the MB image included; a model the seed marks
vendor_unitruns the vendor’s firmware and is named in the reason instead of being required) · BOM imported, no unresolved line · Required documents · Seed set and reconciled · Bench pass recorded (at least one bench pass record that matches the seed as it stands now, below; a record taken before a draft seed was edited fails it with “The bench pass of <date> tested other firmware than this seed certifies (…)”). Each item is Pass / Warning / Fail with a one-line reason and links to its card below; warnings do not block. - Bindings: Role (name and handshake key), Accepted model (new-tab link, with its Active / Retired status), Port → channel (“A → 1 Ceiling dimmer”), or “no channels (accepted only — the gateway)” for boards that deliver nothing.
- Documents — five fixed slots, PDF only: Assembly guide, Overview and Channel map are required; System diagram and Build sheets are optional. (The bench evidence is no longer a document slot: it is a bench pass record, below.) Each slot shows its current file (name, size, sha256, who and when), Open (new tab: a 10-minute link to the PDF in Nova's private bucket, shown inline and logged as
artefact.link) and, for ADMIN, Upload… / Replace…. A replacement keeps the previous file in the history and is allowed after release too (a typo fix changes nothing about built units). A file that is not a PDF is refused withnot_pdf. There is no BOM document slot: the BOM card is the BOM. - Bill of materials — the title carries the facts (“42 lines · 2 unresolved · covers 7 units”), the line underneath who imported it and when. One row per line: Code (class, role for boards, reference), Part (maker, MPN or the harnesses it is used in), Qty, required / optional, Resolution (Resolved, or Unresolved with the reason: a
[..]placeholder, no item code, a board model the config does not accept, no revision of it in the models master yet, an item code that is not a valid SKU, a unit mismatch, a vendor unit named the wrong way) and In stock (what stock supplies today; hover for the units it covers). Vendor-unit roles (ruled 2026-10-23): a role whose accepted models are all vendor units (VM of SB/M1) is filled by a required PART line whose code is the vendor unit's revision (VM/M1/RevA), whose role column is the role (VM) and whose quantity is 1 — a BOARD line, the model code (VM/M1), a missing role or another quantity stays unresolved with the reason; only board lines and these vendor lines name a role. Without that line the checklist item BOM imported, no unresolved line fails with “bom_vendor_part_missing: …”. The import takes the bench csv (hardware/<code>_BOM.csv); part and consumable item models the Inventory does not know are created from the lines. The IoT core stores no BOM: the card reads the Inventory in-process, and the whole card is hidden withoutinventory.read. Frozen once the version is released (bom_frozen). - Seed — the supervisor seed the assembly station writes into a unit (
configs/v<N>/product_config.json): “What the box runs: checked against the board profiles at every save — it may promise less, never more. Frozen at release.” Folded behind “product_config.json” with a copy button and, once set, Download product_config.json (the file as stored, namedTTB-M1-v1.product_config.json, with its SHA-256 in a header). For one unit, prefer the unit page’s Download Pi seed ornova-station seed --unit: both serve the unit’s own released, signed version and refuse a draft.- Generate from bindings… (ADMIN, drafts;
?action=edit-seed&seed=generate) opens the editor already filled with a draft built from the catalogue objects — channel table (with defaults), bindings, port and service profiles, the current releases ascertified_fw. Nothing is stored until you save; fill what the objects do not hold (kiosk panels, topology details). Inside the editor the same button refills the text; over unsaved edits it first asks “Replace your unsaved edits with a generated seed?” (Keep editing / Replace). - Set seed… / Edit seed… (ADMIN, drafts;
?action=edit-seed) opens the JSON editor in place. Save seed sends it; the backend reconciles it against the profiles and refuses a seed that promises more than the boards express. The refusal appears beside the JSON (under it on a phone) in red with its message and its kind in mono plus a copy button, e.g. “Model PB/M1: certified_fw PB/M1/RevE 0.3.1 is not a published release built from the model’s current profile” ·seed_fw_unknown. Kinds:bad_seed(shape, product model, version),seed_channel_mismatch(a channel differs from the table),seed_model_not_accepted,capability_not_supported/constraint_exceeds_port(a binding asks a port for more than it can),seed_port_mismatch(ports or electrical facts differ from the profile),seed_fw_unknown,seed_model_attr_mismatch(vendor unit, topology, requests),seed_service_not_in_model,seed_surface_not_in_model(a kiosk output),seed_role_not_hosted(a virtual board the master does not host),seed_capability_not_driven. Fix what it names and save again; the text stays as typed. - At release the seed is frozen and signed with each accepted model’s profile fingerprint and a
masterblock. A released version shows a Frozen at release card: Master model, Min image (the lowest current Pi image release carrying the master’s current profile) and Master profile (short fingerprint, copy), then a Role · Model · Profile fingerprint table (“none (vendor unit)” for a vendor’s board). Versions released before this rule carry no master block and say so.
- Generate from bindings… (ADMIN, drafts;
- Bench passes · N — “A run of this combination on a test device: the device, the Pi image, every board firmware. Release needs one that matches the seed as it stands.” Paged (
?page=&size=), newest recorded first; sort by Tested or Recorded (?sort=tested|-tested|recorded|-recorded). Columns: Tested (when the run happened), Device (the test box’s Pi — an ERP board row, new tab — with the notes), Pi image (the MB release, e.g.MB/M2/RevA/1.5.0), Board firmware (one line per revision · version), This config — Matches this config (green: it tested exactly what the seed certifies and unlocks the release) or Does not match (red, with what differs underneath, e.g. “PB/M1/RevC is not a revision the seed certifies (expected one of: PB/M1/RevE 0.3.3)”; a record taken before the rule of 2026-10-24, or before a draft seed was edited — records are append-only and are never rewritten, they simply stop counting), Recorded (by whom, when — the operator), Report (the PDF’s name, size and SHA-256, or “no PDF”) and the row verb Open (the PDF in a new tab) or Attach PDF….- Record bench pass… (ADMIN;
?action=record-bench-pass; draft or released) — pre-filled from the seed, you confirm rather than type (ruled 2026-10-24: a bench pass must match the config version it unlocks): Test device — a picker of the ERP board rows of the accepted MB model (the test box’s Pi), or Scan or type a serial… (checked against the Inventory); Tested at (your local time; not in the future); Pi image release — a picker of the published releases of the MB model at or above the minimum (the seed’smaster.min_releaseonce released; on a draft the lowest current release built from the model’s current profile, the rule the release freezes), the current one chosen; Board firmware under test — one row per board role, the Revision chosen among the revisions the seed certifies (certified_fw; vendor units such as VM/M1 with the vendor’s version) and the Firmware shown read-only from the seed; optional Notes. When nothing can be recorded yet the dialog says why (no seed, no current MB image, a role the seed certifies nothing for) and Record bench pass stays greyed out. The record is audited and append-only, under your name. Refusals are shown at the top of the dialog with their kind:bad_bench_pass(a field missing or wrong),bench_pass_no_seed,bench_pass_board_not_in_config(a revision of a model the version does not accept),bench_pass_fw_mismatch(a revision the seed does not certify, or a version other than its certified one — expected vs given),bench_pass_incomplete(a board role with no firmware named),bench_pass_mb_release(not a published release of the MB model, or below the minimum),bench_pass_device_unknown(no such board in the Inventory),bench_pass_device_wrong_model(a board of another model than the MB). - Attach PDF… (ADMIN;
?action=attach-bench-pdf&record=<id>): the bench report, PDF only, once per record (bench_pass_document_setafter that; a later report needs a new bench pass);not_pdffor anything else. - When: after the seed is set and the combination ran on a bench box with that seed — before Release…, which stays greyed out until a matching record exists. Editing a draft seed afterwards (another certified firmware) makes the record stale: run the bench again and record it. Later the test-device flow will write this record itself.
- Record bench pass… (ADMIN;
Release… is a red dialog with a reason (irreversible): “The assembly line will offer this version; the seed and the BOM freeze. A change of recipe after this is a new version.” Releasing writes the profile fingerprints and the master block into the seed (see Frozen at release). The backend refuses otherwise with not_releasable naming the failing items. Make current… confirms: “New units are built to the current version; the assembly line picks it by default.”
The builder (/products/models/SB/M1/configs/new, ADMIN, published models)
Its own page, “New config version of SB/M1 — will be vN”; ?from=N (or New version from this one… on a version page) seeds it with that version’s roles and bindings. “Per board role, pick the accepted board models and bind their ports to the channels. Every channel needs one role.”
- The coverage strip under the toolbar shows every channel as a chip: green = delivered by one role, grey = nobody delivers it yet, amber = two roles deliver it or not every model of the role does; “12 of 13 delivered” at the end. The backend applies the same rule on save.
- Add role…: start with the master board, then every board role the product is built from; the new block takes the focus. “The Pi accepts any model listed here.” Remove role drops the block.
- Inside a role, Add model… lists the active board models of that role not yet added (disabled with “No further active models of this role” when none are left). A role with no model shows “Add at least one model.”
- Inside a model block, one row per port: Port letter and default name, its Capabilities, and a Delivers channel drop-down offering “unused” plus only the channels this port can cover; a channel bound to another port of the same model is listed but disabled “(bound to another port)”, one another role already delivers “(another role delivers it)”. “No channel needs only what this port offers.” appears when nothing fits. Auto-fill gives each still-unbound channel to the first free port that covers it, in channel order, skipping channels another role already delivers. A model without ports says “No ports: accepted only (the master board, for example).”
- Optional Notes (why this version exists). The checkbox Make it the current version (new units are built to it) is off by default: a new draft is not the line’s recipe until it is released and chosen.
- Create vN (the Save / Cancel bar stays at the bottom of the window). Backend problems that name a model (“Model PB/M1: …”) are shown in red on that model’s block; others (“Channel 3 …”, “Role PB: …”) as a banner. On success you land on the new version’s page; Cancel returns to the model’s configs tab.
- Two views: Form and JSON — the roles as text in the shape of the bench file’s
console.roles(config.json):[{ role, models: [{ model, bindings: [{ port, channel }] }] }]. Switching back resolves role codes and board model codes against the catalogue and shows a shape error under the box. At most one SELF-check role (the gateway) may be accepted; the backend refuses two withmultiple_self_roles.
Boards: roles and revisions
What: the hardware catalogue the factory builds from: “Board roles, their models and hardware revisions, and the firmware releases of each revision.” Identifiers are generated; only release versions are typed. Where: /boards. Who: every staff role reads; ADMIN creates and edits. When: when hardware engineering defines a new kind of board, a new generation, or a new PCB revision.
The chain
| Level | Example | Means |
|---|---|---|
| Role | PB | What a board is for (power, LED, display, master). Code and name never change. |
| Model | PB/M1 | The functional contract: ports, capabilities, limits. One per hardware generation. |
| Revision | PB/M1/RevE | A PCB that implements the model. Letters follow the hardware team’s lineage. |
| Release | PB/M1/RevE/0.3.0 | Firmware for one revision. The version is the only identifier you type. |
Tab “roles” (default)
Code (links to the role page), Name and description, Handshake key, Check mode badge (hover for the meaning), Models (count → the role’s page), Created.
New role… (ADMIN, primary button, ?action=new-role): Name (up to 120; the Code is proposed from its initials, override with 1–6 letters or digits), Check mode, optional Description. “The code becomes the prefix of every model, revision and release under it and cannot change later.” Errors (name required or taken, bad or taken code) land under the fields. You land on the new role’s page.
| Check mode | Meaning |
|---|---|
| Handshake | The board announces itself to the master with its handshake key at power-on; the master verifies and updates it. |
| Self-check | The board checks its own image against the seed at boot; nothing announces it. |
Tab “revisions” (?tab=revisions)
The one global list of revisions, paged: search by code or notes; filter by role, then by model (the model filter is disabled until a role is chosen — “Choose a role first” — and clears when the role changes), and by status (Active / Retired); sort by Code, Status or Created. Columns: Code (PB/M1/RevE → revision page), Role and Model (links), Status dot (hover for Active / Retired), Current release version, Created. There is no “New revision” here: revisions are added from a board model’s Revisions tab.
Board role page
Where: /boards/roles/<CODE>, e.g. /boards/roles/PB. Breadcrumb Boards › PB. Header: name with the check-mode badge, code with copy, handshake key with copy, Created, Updated; Delete… (ADMIN, greyed out with “Roles with models cannot be deleted” while it has models). Tabs: overview · logs.
Overview
- “Role” card: Name and Description (pencils, ADMIN), Check mode with its explanation (pencil while the role has no models; afterwards “Fixed once the role has models” — the backend answers
role_in_useif forced). The code never changes. - “Models” card, right under it: “Hardware generations of this role; each has its own revisions.” Code (→ model page), Name, Status (Active / Retired), Revisions count, Created. New model… (ADMIN): “Will be PB/M2. Model numbers are assigned in order and never reused.” Name required (up to 120; a name already used in this role is reported under the field), Description optional. You land on the new model’s page, where the next step is its port profile.
- Delete…: a red-button dialog with a reason. You return to Boards.
Logs
Role created / updated / deleted, with the staff member and reason.
Board model page: profile, revisions, products
Where: /boards/models/PB/M1 (by code). Breadcrumb Boards › role › PB/M1. Header: name, status badge (Active / Retired), an amber Profile fixed badge once the profile is frozen, full code with copy, Created, the retire reason when retired; Retire… / Restore… (ADMIN). Tabs: overview · profile · revisions · products · logs.
Overview
“Model” card: Name and Description (pencils, ADMIN), Role, Ports (count → Profile) and Revisions (count → tab). “Timeline”: Created, Updated. Whether the profile is still a draft or fixed is the amber Profile fixed badge in the header and the line on the Profile tab.
Retire… (reason; reversible, not red): “No new revisions can be added while retired. Existing revisions and releases are untouched. Reversible with Restore.” Restore… (reason): “The model becomes active again and accepts new revisions.”
Profile tab — what the model promises (?tab=profile)
What: the model’s promise in one place: the cards below, in this order. Who: every staff role reads; ADMIN edits until the profile is fixed. When: right after creating a model, before its first release — every release must be built from this profile, and once a release is published or a product config accepts the model the whole tab is fixed (a different promise is a new model). While fixed, each card says so in text: “Fixed: a release is published or a product config accepts this model. A different promise is a new model.” and its edit button is greyed out; on a retired model “Model is retired — restore it to edit.”
- Profile (read-only): Kind — “Port profile (handshake board)” or “Service profile (self-check board)” (the MB master board) — and the Fingerprint: the first 12 characters of the SHA-256 of the canonical profile and protocol number (hover for all 64, the copy button copies it). Every firmware or Pi image release of the model must declare exactly this fingerprint (see the revision page); on a draft profile it changes with every edit. Download profile.json saves the profile as the firmware and image builds consume it (
PB-M1.profile.json: model, kind, proto, profile, fingerprint, frozen); the build generates its port table from it and writes the fingerprint into the release manifest. - Service profile (self-check models only, i.e. the MB master board): “What this master-board model promises: its services, kiosk surfaces, hosted virtual boards and the capabilities it drives.” Services (badges from the closed list
ble,kiosk,video,streaming), Surfaces (display outputs such asHDMI-A-1· kiosk_surface), Hosted roles (virtual boards the image runs, e.g.video-board), Drives (the channel capabilities the supervisor commands). “No service profile” until set. A seed is checked against it: a service, kiosk output, virtual board or channel capability the model does not promise is refused. - Port profile: “The ports this model promises, what each can do, its limits and electrical facts.” Columns: Port (lettered A, B, … in order), Load (the schematic load the port is wired to), Capabilities, Physical limits (e.g. “level 0–100 · 2700–6500 K · sources: hdmi1, hdmi2”), Electrical (“24v · 3 A · PWM”, or —), Default kind, Default name. A model with no ports shows “No ports” (on a master board: “its promise is the service profile above”).
- Attributes: “What a seed’s vendor unit, topology and requests are checked against. Fixed with the profile.” Vendor unit (Yes — the model fronts a unit from another vendor, a virtual board the Pi runs, such as a video mixer — or No), Topology (the model’s fixed wiring as JSON, folded) and Requests (the device requests it answers, e.g.
{"video.stream": {"tiers": […]}}).
Service profile editor (?tab=profile&action=edit-service-profile, ADMIN, self-check models, until fixed)
- Set service profile… / Edit service profile… opens the editor in place. Form: tick the Services; Add surface adds a row (Output name such as
HDMI-A-1, the Kiosk surface tick; ↑ ↓, Duplicate, Remove like every row list); Hosted roles as handshake keys, comma-separated; tick the Drives from the capability list. - JSON: the same profile shaped like the model file’s
console.profile—{ services, surfaces: [{ id, capabilities }], hosts_roles, drives }— so the repo’sMB/<Mn>/model.jsonprofile pastes straight in; switching back parses it and shows a shape error under the box. - Save service profile (sticky bar). A refusal (unknown service, surface capability or drive;
profile_frozenonce fixed) is a banner with its kind and a copy button. Cancel asks before discarding.
Attributes editor (?tab=profile&action=edit-attributes, ADMIN, until fixed)
Edit attributes…: Form = the Vendor unit tick plus two optional JSON boxes, Topology and Requests (empty = none; a box that is not a JSON object is marked under it); JSON = { vendorUnit, topology, requests }. Saved whole with Save attributes; refused with attributes_frozen once the profile is fixed.
Port profile editor (?tab=profile&action=edit-profile, ADMIN, until fixed)
- Two views: Form (rows) and JSON (the same profile as text; port letters follow the order of the JSON). Switching back parses the JSON; a shape error is shown under the box.
- Add port adds a row and moves the focus to it. Per row: the letter at the top left is its position (1 → A, 26 → Z, 27 → AA; text, not a field); Load — the schematic letter (up to 8 characters, optional) for the people wiring the box; Default kind from the vocabulary; Default name (required, up to 60, e.g. “Socket A”); ↑ / ↓ (moving renumbers), Duplicate, Remove (a row that holds something asks first).
- Electrical facts (optional, documentation for the supervisor, not part of the fingerprint): Power (the rail, e.g.
24v), Max A (amps the port may carry), PWM (Not stated / Yes / No). In JSON they arepower,maxA,pwm. A seed’s port facts must match them (seed_port_mismatch). - Capabilities: ticking one seeds sensible Physical limits you then edit — Level range 0–100, Colour temperature range 2700–6500 K, Colour model rgb, Pulse duration range 1000–30000 ms; Sources has no default and is typed as a comma-separated list (“hdmi1, hdmi2, usb-c”). Unticking removes its limits.
- Save profile (the Save / Cancel bar stays at the bottom of the window). A backend complaint about one port (“Port C: …”, e.g.
bad_port_electrical) is shown in red on that row; anything else is a banner. Cancel asks before discarding.
Revisions
“Hardware revisions of this model, in order; firmware releases hang off a revision.” Columns: Code (→ revision page), Status, Current release, Releases count, Notes, Created. New revision… (ADMIN) is disabled on retired models (“Model is retired — restore it to add revisions”).
New revision dialog: “Will be PB/M1/RevB. Type the hardware team’s letter when the silkscreen and the schematic carry one; otherwise the next letter is assigned. Letters are never reused.” Hardware letter is optional and must look like RevA … RevZZZ (“Must look like RevA … RevZZZ” otherwise); the preview in the dialog follows what you type. Notes: what changed in the hardware (up to 2000). When: after the hardware tests on the PCB have passed and before any firmware for it is released.
Products
“Product models whose config versions accept this board model. Any reference fixes the port profile.” Product (link to its Configs tab), State, Config versions that accept it, Current (vN, “not in current” when the product’s current version no longer accepts this model, or “—”).
Logs
Created, updated, port-profile, service-profile and attribute changes, retire / restore.
Board revision page and firmware releases
Where: /boards/revisions/PB/M1/RevE (by code). Breadcrumb Boards › role › model › RevE. Header: the full code (PB/M1/RevE) with copy and the status badge; underneath the role and model names, “Current release 0.3.0” or “No current release”, the retire reason when retired, and Boards in the ERP (the Inventory’s boards of this revision, new tab). On the right: Flash… (the primary, for anyone with board.flash; replaced by “Retired: not flashed.” or “No current release to flash yet.” when it cannot run — see Flash from the console) and the Actions menu (ADMIN): Upload release…, Register image… (both greyed out with “A retired revision takes no new releases.” when retired), Retire… / Restore…. Tabs: overview · logs; the old ?tab=releases links land on the overview. A gateway revision (its role’s check mode is SELF: the MB, the Raspberry Pi) has no Flash…, no Upload release… and no Flash procedure card: its releases are Pi images, registered by reference and written to a card by the station (nova-station image, scope pi.image) — see “Pi image releases” below.
Overview
The releases are the body of the page, with a “Revision” card beside them: Notes (pencil, ADMIN, up to 2000), Created, Updated. Retire… (reason; reversible, not red): “No new releases can be added while retired. Existing releases keep their state. Reversible with Restore.”
Flash procedure
Under the Revision card: what the station agent does and tells the operator when flashing this revision — Chip (esptool id), Download mode (Automatic (DTR/RTS) or By hand), the Operator text shown when the board is not in download mode (BOOT-hold instructions), After flash (Nothing or Press RESET on the board), Identity check (whether the agent asks the board for its firmware after writing, how long it waits, and an optional extra pattern), Bauds (serial · flash), App offset (where the app partition starts in the factory image; the OTA image is cut there) and Notes. “Not set” means the agent uses its built-in defaults for the role. The engineer normally sets it from the firmware repo (tools/catalogue_sync.py apply reads revision.json → flash); ADMIN can also edit it here: the pencil opens the form in place, and the Form / JSON switch shows the same values in the file’s own shape (snake_case, _what-style keys ignored), so the repo file pastes in. Invalid values are refused with the field named. Clear flash procedure… (Actions menu, when set) returns the revision to the agent’s defaults.
Releases
“Releases · N — newest version first. Publishing the first release of any revision fixes the model’s port profile.” The order is SemVer precedence, not upload time (see Versions and environments). Columns: Version (with a green current badge and the notes; on a gateway revision also the tag badge Dev / QA / Prod / Dev (TEST keys), see Versions and environments), State — the release state and, under it, the profile badge: Profile current (green: built from the model’s current profile), Profile stale (amber: built from an older profile — rebuild against the current one and upload again) or No profile (amber: the release declares none and cannot be certified in a seed), with the short fingerprint and proto underneath (hover for the full value) — Source (upload, or external with its URL), Size, SHA-256 (short, copy button for the full value; under it OTA with the hash of the update image — the bytes of the factory image from the app offset to the end, computed by the server when the file was uploaded), Embeds (other board releases an image ships), Released when and by whom (or “uploaded …” for drafts), and the row verbs. A link that names one release (?release=<id>) highlights its row and puts its version in the tab title. On a retired revision the card says so and the row verbs are greyed out.
| State | Meaning | Row verbs |
|---|---|---|
| Draft | Uploaded or registered, not yet available to devices. | Publish… · Withdraw… · Factory image / OTA image (uploads only; the links answer only once published) |
| Released | Available to devices and to the flashing station. One released version per revision is current. | Make current… (when not current) · Withdraw… · Factory image · OTA image |
| Withdrawn | Pulled with a reason; it cannot be published again and is never flashed again (the station checks the current pointer on every job); its bytes are deleted. | — |
- Upload release… (ADMIN, Actions menu): choose the Factory image (the
.factory.binfromtools/release.py: bootloader + partition table + app, flashed at address 0 by the station), type the Version (“The only part you type; unique within the revision, bare x.y.z (e.g. 1.2.7): board firmware carries no environment tag, the same binary is promoted.”), the Profile fingerprint (64 hex characters) and Proto (the ICD-2 protocol number) from the build’s release manifest — both required, the Upload button stays grey until both are valid — and optional Notes. It starts as a draft. A missing declaration is refused withprofile_required, a fingerprint that is not the model’s current one withprofile_stale(naming both fingerprints): shown in red right under the two fields with the kind and a copy button. Refresh the profile file from the model page (Download profile.json), rebuild and upload again. The server checks that an ESP app image sits at the revision’s app offset and hashes the OTA slice from there; the OTA.binitself, or any other file, is refused (“Not a factory image …”). Field errors: version taken or malformed, file empty, too large or not a factory image. - Factory image / OTA image (row links, published uploads only): download the file the station flashes (
PB-M1-RevE-0.3.1.factory.bin) or the update image a box serves to its boards (PB-M1-RevE-0.3.1.bin, the name the Pi looks up). Every stored file lives in one private bucket (nothing in it is public): the link sends your browser on to a short-lived download link (10 minutes) for exactly that file, and each link issued is a row in the revision's log (artefact.link: who, which file, until when). A draft or withdrawn release answers “release_not_published”, and a file missing from the bucket answers 404 “artefact_missing” rather than an error page. - Register image… (ADMIN, Actions menu): for a vendor image hosted elsewhere (https:// only; Nova's own files are uploaded): Version, Size in bytes, SHA-256 (exactly 64 hex characters), Image URL, optional Embeds (“full codes of the board releases this image ships, comma-separated (PB/M1/RevC/1.4.0)”), the required Profile fingerprint + Proto (as for an upload;
profile_required/profile_staleshown under them), Notes. “The checksum and size are recorded so devices can verify what they fetch.” An unknown embedded release is reported under Embeds; an http:// or s3:// URL is refused (bad_url). - Publish… (red: it cannot be unpublished): “Publishing makes the release available to devices and fixes the model’s port profile for good. It can be withdrawn later, not unpublished.” The checkbox Make it the current release for this revision is ticked by default.
- Make current…: a confirm — “Stations flash the current release; boxes update to it. The previous current release stays published.”
- Withdraw… (red, reason): on the current release the dialog warns “This is the current release: the revision will have no current release until another is made current. Devices already running it are not affected.”; on any other, “A withdrawn release cannot be published again.”
- Download (uploaded releases only) sends your browser on to a 10-minute link to the file in the private bucket; the link is logged on the revision (
artefact.link).
Pi image releases (gateway revisions)
The header’s “Current release 1.2.0-dev.2” carries the same tag badge on a gateway revision. On a gateway revision the card reads “Pi image releases · N” and the way in is Upload Pi image… (the Actions menu, or the empty state's button; ?action=upload-image): the Disk image (what the station writes to a new card) and, optional, the OTA bundle (the signed RAUC file of the same version that a box in the field installs; without it the image can be written at the station but never offered over the air), Version — SemVer with the environment tag as image.json names it, and only this console's own class: 1.2.0-dev.1 on the dev console, 1.2.0-qa.1 on QA, bare 1.2.0 on production; another class or a malformed version is said in red under the field before anything uploads (“A QA build: this dev console holds x.y.z-dev.N only.”) and Upload stays off, and the backend refuses it too —, optional Embeds, the required Profile fingerprint + Proto and Notes. Both files go straight from your browser to the private bucket, never through the backend, so an 800 MB image is fine (at most 5 GB per file): the console first checks each file (its SHA-256, “Checking … %”), then uploads it on a one-file link bound to that exact size and checksum (“Uploading … %”; the bucket refuses any other bytes), and creates the draft release from the two uploads. Cancel upload stops it; nothing half-uploaded is ever used (an unused upload is deleted by the bucket after a day). Refusals: upload_incomplete (the file never reached the bucket), upload_checksum_mismatch, upload_not_yours, upload_used, version_taken. The row shows the image's file name, its SHA-256 and the bundle's; published rows link Image and Bundle (short-lived links, logged like every download). Register vendor image… registers a vendor image by its https URL instead (the station fetches it itself; never offered to boxes). A multipart upload to a gateway revision is refused (gateway_image_by_upload_link).
The SHA-256 cell shows Bundle with the bundle’s hash (copy) under the image hash, or “No OTA bundle”. Publish… and Make current… work as for boards: the station’s nova-station image --revision MB/M1/RevA asks for the current image and never writes another. A published image with a bundle has the row verb Offer to devices… (ADMIN; greyed out without a bundle): paste up to 50 device serials (one per line; duplicates folded), type a reason, and Offer to N devices. Each serial is offered on its own, exactly like Offer update… on the device page; the result lists every serial as offered or not offered with the reason (unknown serial, revoked, built on another MB revision, the box already runs this version or newer …). There is no fleet rollout or scheduling yet.
When: the engineer builds from a tagged commit (tools/release.py), uploads or syncs the release (tools/catalogue_sync.py can create role → model → revision → release from the firmware repo’s JSON and publish / make current in one go, sending the profile fingerprint and proto itself; the dialogs are the manual path), publishes it, and makes it current before the factory flashes boards of that revision. Making a release current is what moves the station’s and the Pi’s “current” pointer.
Versions, environments and badges
What: there are three Nova clouds — dev, QA and production — each with its own console, its own keys and its own station agent. A Pi image says which keys it carries in its version, as a SemVer pre-release tag (ruled 2026-10-26), never in the major number:
| Version | Built with | Held by | Badge |
|---|---|---|---|
1.2.0-dev.N | TEST keys: the dev profile and the prod-profile bench rehearsals (--bench-egress / --bench-ssh) | the dev console | Dev |
1.2.0-qa.N | the QA profile and QA pins (--trust build/qa/trust.env) | the QA console | QA |
1.2.0 | production keys (no tag), stamped so in its image.json (test_keys false) | the production console | Prod |
1.0.14 | no tag, but its image.json says TEST keys (test_keys true): the bench images from before the tags | history on the dev console | Dev (TEST keys) |
- Order = SemVer precedence everywhere the console sorts or says “newer”:
1.2.0-dev.3 < 1.2.0-qa.1 < 1.2.0, and the number after the tag compares as a number (-dev.10is newer than-dev.9). Release lists are newest version first, the Offer update… list greys out what is not newer than the box, and the box itself refuses an older one (not_newer). A version that is not SemVer sorts last. - One class per console: the build refuses a tag that does not match its keys, and each console takes only its own class — a dev console never holds a
-qa.Nimage, QA never a-dev.N, production only bare versions. - Board firmware (PB, LB …) stays bare
x.y.zin every environment: the same binary is promoted, the environment is simply which console holds it, so board releases carry no tag badge. - The badge (Dev / QA / Prod / Dev (TEST keys), neutral grey: a category, not a state; hover for what it means). A bare version reads Prod only when the release's own stamp says prod keys (the uploaded image.json's
test_keys); stamped TEST keys read Dev (TEST keys), and an unstamped bare version shows no badge — nothing is called Prod by guess. The device page knows only the running version, so a bare one shows no badge there. It sits beside a Pi image's version on the gateway revision page (header and rows) and on the device page's Pi image card (Running). - The station agent carries its environment in its program name, not its version:
nova-station-dev,nova-station-qa,nova-station, each from 1.0.0 (see Agent installers and the Station agent book).
When: the image line restarted at 1.2.0-dev.1 (dev) and 1.2.0-dev.2 (the bench rehearsal); the first QA image is 1.2.0-qa.1. Older 1.0.x / 1.1.x records stay as history.
Devices: the registry, transfers
What: “Every unit the factory registered: a product model, a config version and a claim label kept as hashes.” Devices are born by device ingestion (the Inventory’s Pre-register), never by hand. Where: /devices. Who: every staff role reads; ADMIN imports and acts. When: after the factory has pre-registered units; daily for support questions about a serial.
Tab “devices” (default)
- Search by serial or identity fingerprint. Filters: model (one drop-down: “All Studio Box models” for a whole line, or one model), state (Registered / Claimed / Revoked), policy (Locked / Open), activation (Activated / Never activated). Sort by Serial, State, Policy, Activated or Registered (newest first by default).
- Columns: Serial (→ device page) with the short fingerprint, Model and name, Config vN, State badge, Policy badge, Owner (new tab, Accounts section; “factory test” underneath when the claim is a factory test), Label version, Activated, Registered. On a phone the rows stack into cards.
| State | Meaning |
|---|---|
| Registered | Pre-registered by the factory; claimable with the label. |
| Claimed | Bound to an owner. |
| Revoked | Platform block: cannot be claimed and is refused at connect. Reversible with Restore. |
| Claim policy | Meaning |
|---|---|
| Locked (default) | The owner binding survives a reset; only the owner or a cloud-side release changes it. |
| Open | A claim after a reset auto-releases the previous owner (reserved for a retail SKU). |
Tab “transfers” (?tab=transfers)
Every transfer offer between owners — support’s answer to “my code says expired”: Created (opens the offer’s page) and offer id, From, To (“not yet redeemed” while pending), Devices (first three serials, “+N more”), Status, Expires / closed. Filter by status.
Transfer offer page — /devices/transfers/<id>
Breadcrumb Devices › Transfers › id. Header: “Transfer <short id>” with the status badge; from and to (owner links, new tab), created. “Devices · N”: every device in the offer (serial → device page, model). “Timeline”: Status with its meaning and the cancel reason, Created, Expires, Accepted, Closed, Id, Sender. Read-only: an offer is made and accepted in the mobile app.
| Transfer status | Meaning |
|---|---|
| Pending | The code is redeemable; the devices keep working under the sender until it is accepted. |
| Accepted | Every device moved to the receiving account in one transaction. |
| Cancelled | Withdrawn before redemption (by the sender, replaced by a newer offer, or a device left the sender’s ownership). |
| Expired | Not redeemed within the offer lifetime (72 h). |
Device page
Where: /devices/<serial> (by serial; an id address moves there), opened from the registry, an owner’s Devices tab, an ERP unit or a transfer. Breadcrumb Devices › serial. Header: the serial (copy), the state badge, an amber “Open policy” badge when applicable; underneath the product model (link), config vN (link to the version page), the config badge (below) and the live strip — the presence dot, online / offline, “seen 2 min ago”, “N facts pending”, “N facts refused”. On the right (ADMIN): Change policy… (the primary) and the Actions menu: Push config…, Offer update… (greyed out with “Revoked: refused at connect, so it cannot be updated.” on a revoked unit), Unbind… (greyed out with “Not claimed by anyone.” when there is no owner), New label… (greyed out with “Bound to an owner — release the device before issuing a new label.” while claimed), Revoke… (red) / Restore…. Tabs: overview · connection · media · claims · logs. SUPPORT sees every card and no verbs.
Overview cards
| Card | Shows | ADMIN action |
|---|---|---|
| Unit | Identity fingerprint of the certificate on the Pi (copy) — “Pending” after a gateway swap (the old certificate was revoked at the swap, the removed Pi is refused at connect, until the station activates the new Pi’s certificate) —, Revoked (when and why, revoked units only), Claim policy with its explanation, Activated (or “never (first owner claim)”). | Change policy… (header) |
| ERP | Unit (the serial, opening the Inventory’s unit in a new tab for staff with inventory.read), Registered (when the ERP pre-registered it) and, for staff with inventory.read, Claim and transfer: “Allowed”, or “Refused — in for repair (RMA) …”, “… its last device EOL has not passed” or “… taken out of service (scrapped)”: what the owner app is answered (not_claimable / not_transferable with that reason; the factory-tester claim of a device EOL run is exempt). | — |
| Ownership | “One owner, bound by a claim with the label; ends by release, support unbind or open-policy reclaim.” Owner (new tab), Kind (Owner / Factory test with explanation), Owner’s name for the unit, Claimed, Remote control (read-only: the owner’s own setting — App and integrations · Integrations only, where the owner app sees the box but does not control it · App only, where partner keys are refused; with “Not in effect …” while the owner’s integrations cannot act, because then both control it; the owner changes it in the app, staff never do), Transfer (pending offer with its expiry and a link to the offer, or “none pending”). “Not claimed — Anyone with the label can claim it.” otherwise. | Unbind… (Actions menu, only while an owner is bound) |
| Claim label | “Only hashes are stored. The plaintext exists on the printed label and in the factory vault.” Live version, Issued. | New label… (Actions menu, not while claimed) |
| Config | What Nova wants the box to run against what it reports (under ERP; the subtitle is the badge’s meaning). Desired: the pinned version and the SHA-256 of its seed (short, copy; “not stored for this version” without a seed). Reported: the version and seed SHA-256 from the box’s last full status, “differs from desired” beside whichever differs, and when it reported (“not reported yet” before its first full status). Last failed apply (only after one): the version, when, and the box’s reason. | Push config… (Actions menu) |
Config badge (header)
What: whether the box runs the product config Nova wants it to run. The box reports its config version and its seed's SHA-256 in every full status; Nova compares them with the device's pinned version and that version's seed. Who: every staff role, read-only. When: after a Push config…, and whenever a box behaves as if it ran other channels. Data: config on GET /internal/devices/{id}.
| Badge | Means | What to do |
|---|---|---|
| Config applied (green) | The box reports the desired version and seed. | Nothing. |
| Config pending (grey) | A config push is queued or unanswered, or the box has not reported its config yet. | Wait for the box to connect; the Facts queue shows the push. |
| Config mismatch (amber) | The box reports another version or seed than desired. After an acked push Nova re-queues it once (the repeat shows on the Facts queue as “repeat after a config mismatch”) and raises the fault Config mismatch on the Box card (“raised by Nova”), logged as device.config_mismatch. | If the repeat does not fix it, read the Config card’s Last failed apply and the box’s Logs; push again once the cause is fixed. The fault clears by itself when the box reports the desired config (device.config_mismatch_resolved). |
When the box cannot load a pushed config it answers the push with a refusal and reports config.apply_failed: the Config card shows it as Last failed apply, the device's log records device.config_apply_failed, and the owner gets the Activity row device.config_apply_failed (“Settings update failed on <box>”) in the app — once per failure: the same version and reason again within 24 hours (the push's retries, a reconnect) is logged here but not sent to the owner again.
Box card (overview, full width under the cards above)
What: what the box last reported about itself in its full status — everything here is the box's word, as of “reported <when>” in the card's subtitle (hover for the exact UTC time). Who: every staff role, read-only. When: support's first stop for “which image / which boards does this box run?” and before a Push config…. Data: GET /internal/devices/{id}/box, fetched with the overview. Presence and the running OTA image are not repeated here (header strip and the Connection tab’s Pi image card).
- Master image — model/revision · version of the Pi image (e.g.
MB/M2/RevA · 1.5.0), its services as labelled chips (BLE onboarding, Kiosk, Video, Streaming) and its profile fingerprint (short, copy button). “not reported” for a bench box without an image manifest. - Seed — whether the supervisor trusts its seed: Signed (green: the cloud signature verified), Unsigned (amber: a bench allowance, never in production), Not trusted (red, with the box’s own status beside it, e.g.
hash_mismatch,wrong_master: the box refused the seed and runs without it). When the box runs degraded the reason is written under it (“Degraded: mb_below_requirement”). “not reported” when the box sent nothing. - Device map — the version of the signed device map the box runs (“not reported yet” while the box has sent none — a fresh gateway Pi reports its map once a config push has seeded it, and an unreported map is never “behind”; “none installed” only when the box itself raises Device map missing): the list of this unit's boards — gateway Pi, each board's serial and Ethernet MAC, the mixer's MAC — that the box serves; any other board is refused. The Inventory's current version is on the ERP unit page.
- Boards — one row per role (handshake key): Role, Model (model/revision as the board announced it, or “not reported”), Serial (copy) with the board's Ethernet MAC under it, Firmware, Profile (short fingerprint, full on hover) and State badges: Online (green) / Offline (grey, with “seen <when>”), Quarantined (amber: the supervisor isolated the board after repeated failures), Unavailable (red: its channels are off, e.g. a profile or firmware mismatch).
- Faults (only when there are any) — one badge per open fault with its details, its channel, “raised by Nova” for a fault the cloud raised itself, and since when; other faults show the box's own code. Staff always see serials and MACs; the owner app only reads “A part of this device is not recognised — service needed.” for the two box faults below.
Fault Raised by Details What to do Config mismatch (amber) Nova — See the config badge above. Foreign board (amber) the box (owner told) role · serial · MAC · firmware of the board it refused A board the device map does not list announced itself: it gets no session and no update, its role shows offline. A swapped board: install the current map at the station (ERP unit › Install device map…). Anything else: find out who opened the box. Device map missing (amber) the box (owner told) reason (missing, bad signature, other unit, other chip) The box has an identity but no valid map, so it refuses every board: run Install identity… (after a Pi swap) or Install device map… at a station. Device map behind (amber) Nova box has vN, current is vM The box runs an older map than the Inventory’s. Expected — and not raised — while the unit is in RMA; outside RMA swaps never happen, so treat it as an integrity signal. Hardware mismatch (red) Nova role: expected serial / MAC, reported serial / MAC (every role that differs) The highest severity: the box claims the current map but runs another board in a listed role, which it should have refused. Take the unit into RMA and inspect it. - Refused boards · N (only when the box reported any) — every board the box refused as not in its device map: Role, Serial, MAC, Firmware, First seen, Last seen, as the box keeps them.
- Empty states: “Nothing reported yet” before the first full status, “No boards reported”, and “Box state unavailable” when the backend does not answer.
Connection tab
“Connection” card: presence (online / offline, last seen), the projection — the cloud’s copy of the box’s channel states, with the device's sequence number (the highest of its channels') and time — and a Channel · State · Seq · Changed by table; “Never connected — no state yet” before the first uplink. Seq is the channel's own sequence: the box stamps every change, and the number keeps growing across box restarts, so a lower number is always an older change (Nova drops an older one that arrives late). Changed by is who caused the channel's last change, in full (staff only; owners and partners see only the kind): Owner app (with the account id), Partner key (with the key id), Nova support (with the ticket), Box screen (the kiosk), Bluetooth at the box (with the account id), The box itself (a physical input, a restore, a board correction); under it the command id that caused it, when there was one (hover the label for the raw actor).
Presence and “degraded”: the console shows the connection as it is (offline the moment the link drops). Owners see a box as offline only once it has been away past the 3-minute debounce: Nova then marks it degraded (the amber offline badge and the “went offline” push in the app); a box that reconnects inside the debounce never looks offline to its owner.
“Facts queue” card (full width): what the registry told the box (ownership orders, config pushes, image offers) and what it still owes it — Type (a config push Nova repeated after a mismatch says “repeat after a config mismatch”), Seq, Created, Status and Refusals. Status: Pending (“not sent yet”, or “sent N times, no answer yet”: while the box is connected Nova re-sends it every minute), Acked (green, when), Refused (red, when: the box refused it five times and Nova stopped sending it), Superseded (a newer fact of the same type replaced it, when — a refused one too, so an older config push can never be re-queued over a newer one). Refusals: how many times the box refused it, with the box's last error under the count. Re-queue… (ADMIN, on a refused row only; ?action=requeue-fact&fact=<id>): fix the cause first, then type a reason and press Re-queue fact — the fact goes back on the queue and is sent again; after five more refusals it stops again. The log records device.fact_requeue with the reason. A fact that is no longer refused answers fact_not_refused and one that is gone fact_not_found in the dialog's banner. “Identity certificates” card: every certificate the Device CA issued for this serial, newest first — Status (Active = what the box connects with; Issued = signed at the station, not yet the identity; Revoked with its reason: superseded or replaced), Fingerprint (copy), Issued (when, by whom, on which station), Valid until. A unit registered with a bench fingerprint shows “No certificate on record”.
“Pi image” card (above the certificates): MB revision — the gateway board revision fitted in this box, from its Inventory build (“no Inventory build on record” for a box registered by hand: offers are then not checked against it); Running — the image version the box booted, from its last full status, with its tag badge when the version carries a tag (Dev / QA; a bare version shows none here, see Versions and environments; “not reported” for a box whose bridge has no update handler yet); Update — the state of the last offer (badge: Never offered, Offered = queued or delivered, nothing heard yet, In progress with the box’s phase in words — Accepted, Downloading · 40%, Downloaded and verified, Waiting for the quiet window (not_in_window / in_use), Installing, Rebooting into the new image, Confirming the new image — then Updated (green), Rolled back (amber: the new image did not prove healthy and the box went back), Failed or Refused (red)), the offered version and when it was offered; On the box — the job the box itself reports in flight; Last result — the box’s last result of any version with its reason. Under it the latest 20 box events (Time · Version · Event · Reason), newest first. Only events about the offered version move the state; an older job’s “failed (superseded)” is listed but changes nothing.
Media tab (streaming and kiosk, read-only)
What: what the box last reported about its video board and its kiosks, so support can answer “why is my stream not starting?” or “why is the kiosk locked?” without the owner’s phone. Where: /devices/<serial>?tab=media, two cards side by side (stacked on a phone). Who: every staff role (ADMIN and SUPPORT) sees the same thing. How: read-only — there are no staff verbs here by ruling: start / stop, stored keys, key reset, the kiosk lock, the PIN and page visibility belong to the owner (app) or the partner (API). Data: GET /internal/devices/{id}/stream and /kiosk, fetched only when the tab is open; times are relative with the UTC time on hover.
| Card | Shows | Empty state |
|---|---|---|
| Streaming | State (Idle · Connecting · Streaming (green) · Stopping) with “since …” and whether it goes to one of the owner’s channels or a partner destination; while not idle, Health (Good green / Poor amber) with kbps and cache %; Last stop reason (e.g. connection_lost); Streaming key — its kid only, never key bytes, with a Verified (green: signed by the box’s identity key and checked against its certificate) or Not verified (amber) badge and when it was reported; Owner channels — how many channels are on the owner’s account and how many keys are stored on the box for kiosk start (counts, never ids or names; “no owner” when unclaimed); Partner — the number of partner destinations and when they expire (“They expire in …”), or “no destinations”. | “This device has no video board” until the box reports a streaming key. |
| Kiosk | Lock (Locked / Unlocked, plus an amber Locked out badge and “the kiosk takes tries again in …” after too many wrong PINs); PIN (Set / Not set — the cloud never holds the PIN, only whether one exists); Auto-relock (after N minutes, or off); Pages shown (Instructions, Camera, Ambience, Setup, Studio (owner), Studio (partner)) with the hidden ones listed; Owner allows (“every page” or its set) and Partner allows (“no restriction” or its set) — a page shows only when both allow it; Reported (when the box last reported its kiosk state). | “This device has no kiosk” until the box reports its kiosk state. |
If the backend does not answer for a card it says “Streaming state unavailable” / “Kiosk state unavailable”; reload the page.
The dialogs (all ADMIN, all need a reason)
- Change policy…: pick Locked or Open (the hint explains the chosen one) and a reason; “Evaluated at claim time.” The button is disabled until the policy differs from the current one.
- Revoke… (red): “A platform block: the unit cannot be claimed and is refused at connect. The owner binding is kept. Reversible.” Restore…: “The unit goes back to the state it had before the revoke.”
- Push config… (
?action=config-push, ADMIN): sends the box the seed (itsproduct_config.json) of a released config version, signed by the cloud; it lands on the Facts queue asconfig.pushand goes down at once if the box is online, else on its next connect. The cloud signs the seed's fingerprint (seed_sha256), not the seed; the bridge checks the signature, then that the seed's SHA-256 matches (seed_hash_mismatchotherwise), validates the seed, writes it and has the supervisor reload. Pick the Config version (released versions only; one without a seed shows “(no seed)” and cannot be picked) and type a reason. Compatibility with the box: as soon as a version is chosen the console checks its seed against what the box last reported (its master model, image version and profile, and every board’s model, revision, firmware and profile fingerprint) and lists the findings with their kind: Refused (red badge:mb_model_mismatch,mb_below_requirement— “needs MB/M2 ≥ 1.5.0, box is MB/M1 1.4.2” —,mb_profile_mismatch,mb_service_missing,board_model_not_accepted,board_profile_mismatch,device_map_stale— “The box runs device map v2; the Inventory's current one is v3 — install it at a station (Install device map) first”), Will update (amber:will_update— a board runs other firmware than the seed certifies; the box updates it after the push) and Warnings (grey:role_absent— no board of a config role is present,no_live_registry— the box never reported). “Every board the box reported matches this seed.” when there is nothing to say. The push button stays greyed out while it checks (“Checking the box…”) and when anything is refused (“Refused: the box cannot take this seed (see above).”). If the backend refuses at push time anyway it answersconfig_push_refusedand the banner lists each reason with its kind. The version it is on (“(current)”) re-sends that seed — e.g. after a box lost it — and the button reads Push config. Another version of the same model moves the device to it (a mixer swap, for example): the hint says “Moves the device from vN to vM”, the button reads Move to vM and push, and the device is re-pinned with the push. Refused withconfig_not_released(a draft),no_seed, andseed_version_mismatchwhen the stored seed names another version (the box would refuse it on every connect). The log recordsdevice.config_pusheach time anddevice.config_version(from → to) on a move. - Offer update… (
?action=offer-update, ADMIN): offers the box a new Pi image. Pick the Image — published gateway image releases that carry an OTA bundle, newest version first by SemVer (“current” marks a revision’s current image; one not newer than the image the box reports is listed greyed out as “not newer” and cannot be chosen), only those of the box’s own MB revision when the Inventory knows it (the hint says which; none yet = “This box is built on MB/M1/RevA; no published image with an OTA bundle exists for that revision yet.”); the hint names the image the box reports and reminds that only a newer version is accepted. The bundle’s SHA-256 is shown for checking; type a reason and press Offer 1.5.0. The offer is the signedota.offerfact on the Facts queue: delivered at once if the box is online, else on its next connect; a newer offer replaces one still pending. The bundle stays in the private bucket: the offer carries a download link valid for an hour (the box starts its download at once; a download that outlives the link — a box that was offline, a resume after a power cut — asks the cloud for a fresh one over its own connection,ota.link.request), and every link issued is logged on the device (artefact.link). The box checks the signature, the URL, the hash and that the version is newer, downloads and verifies the bundle at once, installs it only in its quiet window (02:00–05:00 box time by default, and only when nobody has used the box for 15 minutes), reboots into it and keeps it only if it reaches the cloud — otherwise it rolls back by itself. Progress shows on the Connection tab. Refused withnot_newer(the box already runs this version or newer),release_not_published,no_ota_bundle,not_a_gateway_image(board firmware: boards follow the image),wrong_revision(an image for another MB revision than the one fitted),device_revoked. With no image to offer the dialog says so. The log recordsdevice.ota_offerwith the release, the version and the reason. For several boxes at once use Offer to devices… on the image’s row (Boards › the gateway revision). - Unbind… (red): “Only after the documented human check. The binding ends at once and anyone with the label can claim the unit.” The owner’s name is dropped.
- New label… (red): for a refurb or a lost label. The dialog generates a fresh pair — a 128-bit Claim secret (goes into the label QR only) and a 10-character Claim code (printed beside the serial for manual entry); Regenerate draws another. Type the reason (e.g. “refurb after RMA-17”) and press Issue new label. The old label stops working at once. The result screen “Label vN issued” shows the secret, the code and the QR payload (the claim host’s
/c#serial.secretaddress, from the backend; see “Claim and transfer links”) with copy buttons and the warning “Copy these now. They are shown once. We keep only hashes; the factory vault and the printed label hold the plaintext.” Print the new label after this.
Claims tab
“Internal-only: who held this unit and how each binding ended. Never shown to owners or partners.” Account (new tab), Kind, Door (label QR / printed code / transfer accepted), Name given, Claimed, Ended (with the reason: Released by the owner · Unbound by support · Reclaimed under the open policy · Transferred to another account; a green “live” badge for the current binding). The tab shows its count on every load.
Logs tab
Registered, policy changes, revoke / restore, new labels, claims and refused claims, renames, releases, unbinds, transfers, config pushes, config mismatches and their resolution, failed config applies, refused facts and re-queues, Identity revoked at a gateway swap (the fingerprint that was revoked) and Identity activated by the station (from → to). When a claimed device's identity is renewed the owner gets device.repaired (“<name> was repaired; its identity was renewed”) in the app's Activity and as a push.
Claim and transfer links (the claim host)
What: the two kinds of link an owner's phone opens. A label QR is https://<claim host>/c#<serial>.<secret>; a transfer share link is https://<claim host>/t#<code>. The part after # (the claim secret, the transfer code) never reaches a server. Who: owners, without signing in; staff only set them up.
- One setting decides the claim host: the backend's
nova.claim.host. The label QR (Print label, the QR payload after New label…) and the transfer share links all come from it; the console never writes a host of its own. Productionhttps://claim.novacube.io, QAhttps://claim.qa.iot.novacube.io, localhttp://localhost:3000(this console). - The pages: this console answers
/cand/ton whatever host reaches it, with no sign-in and outside the staff layout. When the Nova app is installed and its link verified, Android opens the app straight away. Otherwise the page explains the link (“Claim your Nova box” / “Accept a Nova transfer”) and offers Open in the Nova app, Get the app and the serial or transfer code to type in by hand, each with a copy button. A label's manual entry also needs the claim code printed on the label: the QR carries the secret, not the code. A missing or damaged fragment shows “This label QR is incomplete” / “This transfer link is incomplete”. The page reads the fragment in the browser only and sends it nowhere. - App Link verification:
/.well-known/assetlinks.json(JSON, no redirect, no sign-in) names the appcom.nova.iot.appwith the signing-certificate fingerprints of this environment. Unset, it answers[]and links open the page instead of the app.
Console settings (environment, read per request)
| Setting | Value | Default |
|---|---|---|
NOVA_ASSETLINKS_SHA256 | The app's SHA-256 signing-certificate fingerprints, comma-separated (AA:BB:…). QA: the QA key 41:DE:E9:39:0E:E3:34:E0:87:DA:E3:03:67:9D:46:B1:E7:B6:9B:8F:F3:62:01:C4:42:2E:E8:8B:AB:21:48:E4. Local: also the developer's debug key (keytool -list -v -keystore ~/.android/debug.keystore). | empty (nothing verifies) |
NOVA_APP_INSTALL_URL | Where Get the app goes; the Play Store listing once it exists. | /docs#app-install |
For QA, DNS points claim.qa.iot.novacube.io at the console and the backend sets nova.claim.host to the same address. A local http link never opens the app by itself (App Links are https only); on the bench scan from inside the app, or use the page's button.
Inventory
What: the physical side of the build: which shipments were received (lots), every board with its serial, firmware and test state, what is in stock, and the models master (board revisions mirrored from Boards, plus the parts and consumables Nova buys). Serials (NVA-PB-00001-3) are allocated here, by the backend, when a station first flashes a board — nobody types one.
Where: the sidebar section ERP: six pages, /inventory/stock, /inventory/lots, /inventory/boards, /inventory/parts, /inventory/devices, /inventory/models (the older /inventory?tab=… addresses redirect there, filters kept). Who: anyone with the inventory.read permission (every ADMIN; a SUPPORT account only when an admin granted it on Users › person › Permissions). Without it the section is not in the sidebar and the pages send you home. Actions need more: inventory.receive for lots, models and part intake, inventory.test for end-of-line results, inventory.assemble to build, pre-register and ship units, inventory.rma for condition changes and repairs.
The six pages
- Stock — two tabs (
?tab=), each a paged list with its filters in the URL. Models: one row per item model (search code, name, maker or part number; filter by class; sort by model or class): Available (ready to assemble; the number opens those boards or parts), Untested (no end-of-line or intake result yet), Faulty / RMA, Used (built into devices). Coverage (?tab=coverage): per imported BOM (product model · version, released or still a draft), the line count and resolution, Units coverable (the smallest cover over the required lines — what today’s stock could build; “—” while any line is unresolved) and Short first (the line that runs out first, with what is in stock against what one unit needs); the product opens its model page and the version its config version page. Search by product model, or model and version (“TTB/M1 v1”); sort by product, version or import time. - Lots — the paged list of shipments: lot code, model, supplier and PO, Recorded / received (boards given a serial against it, out of the quantity received minus rejected), status (OPEN green = new boards are booked against it; CLOSED), received date. Open lots are shown by default; the status filter also offers Closed and Open and closed. Search by lot code, supplier, PO or supplier reference; filter by model. Sort by lot or received.
- Boards — the one global paged list: serial and chip id (“not bound yet” until the first flash), revision (opens the catalogue entry in a new tab), lot, condition with its reason (Not verified grey until the end-of-line test passes, OK green = tested good, Faulty red, RMA amber, Scrapped grey; hover for the meaning), stage, firmware release, when and where it was flashed. Search by serial or chip id (a mistyped serial simply finds nothing: the last character is a check character); filters revision, lot code, condition, stage; sort by serial, flashed, recorded.
- Parts — serialised sourced parts (PSU, switch, router…), born when a lot of a part model is received: serial and vendor serial, model, lot, condition, intake (incoming inspection Pass / Fail / not inspected), the device it sits in, received date. Search serial or vendor serial; filters model, lot code, condition.
- Devices — assembled units: serial, product model (opens Products › Models) with config version, state (Assembled = not yet in the IoT core; Pre-registered; Shipped = left the factory after a passed device EOL; Activated green = an owner claimed it; RMA amber; Scrapped = written off, final), Next step (what the unit waits for: Verify build · Pre-register · Device EOL · Ship · Awaiting first claim · Return to service), device EOL, whether the IoT core knows it (link to the registry entry), assembled when and by whom. Search serial or fingerprint; filters product model, state, EOL result (passed / failed / untested), registered or not, and config version (a select of the chosen model’s versions, greyed out until a model is picked). Assemble (needs
inventory.assemble) opens the assembly page. - Models — the models master, paged (a vendor unit —
VM/M1/RevA— is listed as a Part mirrored from the catalogue): code, class (Board / Part / Consumable), name, maker and part number, serial prefix, in stock, open lots, status. Search by code, name, maker or part number; filter by class; sort by code, class or name.
Stock's Available column means: boards tested OK and free; parts OK and free; for consumables, received minus rejected minus used over every lot, in the model's unit. Used = built into devices.
Receive lot (Lots page, ?action=receive, inventory.receive)
- Click Receive lot…. Choose the Model (board revisions come from the catalogue; parts and consumables from Models — retired models are not offered), type the Quantity received, and optionally Supplier, PO reference, Supplier reference and Notes.
- Vendor units (ruled 2026-10-23: the ATEM mixer is a part, not a board). A model marked “· vendor unit” in the list (
VM/M1/RevA,VM/M2/RevA— a catalogue board model with Vendor unit = Yes, mirrored as a part) asks for a Vendor serial (MAC) of each unit: one input per unit appears once the quantity is typed (Unit 1, Unit 2, …). Type or scan the MAC from the unit's label; it is normalised as you type — upper case, a colon every two digits (aabbccddeefforAA-BB-…becomesAA:BB:CC:DD:EE:FF). Each one is required, and a MAC typed twice in the lot is marked “Typed twice in this lot”; Receive lot stays greyed out until every unit has a well-formed, distinct MAC. The MAC becomes the part's vendor serial, in serial order. Other parts and consumables are received as before, without the inputs. - Receive lot. The lot code (
L-00001) is allocated by the backend; you land on the lot’s page. Errors: “No item model with code …”, “… is retired”, “Quantity must be at least 1”. Vendor-serial refusals appear under the MAC inputs with their kind (copy button):vendor_serials_required,bad_vendor_serial,vendor_serials_count,vendor_serial_taken,vendor_serials_parts_only(see Inventory refusals). - What happens next depends on the class. Board and part lots: one row per unit is born at once, each with its serial (
NVA-PB-00007-x,NVA-PT-00001-x), listed on the lot’s page; print the stickers now and stick one on each unit. Consumable lot: quantity only; assembly draws from it.
New model… (Models page, ?action=new-model, inventory.receive)
Only parts and consumables are authored here; board models appear by themselves from Boards. Class, Code (SKU: 2–40 characters, letters, digits, dot, underscore, dash; unique, upper-cased), Name, Maker, Maker part number, and for a consumable its Unit (pcs, m, set). “An item model with code … already exists” lands under Code.
When: Receive lot the day a shipment arrives, before anyone flashes from it. Stock and Boards are the daily lookup before assembly.
Lot page
What: one shipment. Where: /inventory/lots/L-00047 (by lot code), from the Lots page. Breadcrumb Lots › code; title = the lot code with its status badge (Open / Closed); underneath the model, “x of y recorded”, received and updated. On the right: Print stickers (the primary, board and part lots) and the Actions menu with Close lot… / Reopen lot… (inventory.receive). Tabs: overview · logs (the old ?tab=boards|parts links land on the overview).
- overview — Lot card (model → its Inventory page, supplier, PO and supplier references, Received with rejected and available, Recorded, Notes), then the units table (below). Received is broken down as OK · faulty · not verified, straight from each unit's condition: every unit is born Not verified, becomes OK when its end-of-line test (boards) or intake (parts) passes, and anything else counts as faulty (a board whose chip the station could never read is faulty “dead on arrival”). A consumable lot shows rejected, used and left. Nothing is typed for boards and parts. Notes are edited in place (Edit beside them,
?action=edit-notes,inventory.receive); a consumable lot's rejected quantity likewise (Edit rejected,?action=edit-rejected). Save / Cancel; a dirty form asks before discarding. Close lot… / Reopen lot… (?action=close|reopen) change the status after a confirmation: closed means nothing more is booked against it; close a board lot once every board is flashed so the station stops asking which lot. - Boards / Parts card — every unit of the lot as the lot sees it, paged: serial, condition (not verified until tested, OK once it passed end-of-line or intake, faulty for any other condition) and the reason when faulty. Nothing else: chip id, firmware and history are on the unit’s own page. Consumable lots have no units table.
- logs — the custody ledger for this lot: received, updated, with who and the details.
Print stickers
Print stickers in the header opens /inventory/lots/<code>/print (one print shell for every sticker and label: breadcrumb, Back, Print, the count, and a note when a lot is bigger than one sheet). Board and part lots show one 50 × 25 mm sticker per unit: a QR of the serial, the serial, SKU and name, the lot code. Rule: stick first, then test by scanning. Until a sticker is on, the units of a lot are interchangeable, so any sticker fits any unit; once it is on, that serial is that unit: the station scans it before flashing and binds the board's chip id to it, and scanning a part opens its page where Record intake lands on the right row. A damaged or faded sticker is reprinted from the unit's own page (Print sticker). A consumable lot shows one sticker for the reel or bag: a QR of the lot code (assembly scans it to draw from this lot), SKU and name, quantity received. Print sends only the stickers to the printer.
A lot refuses more boards than it received (minus rejected): the station then reports lot_full. Received cannot be raised afterwards: receive a second lot instead and let the operator pick it with --lot. Rejected cannot drop below the units that already carry a serial — mark those faulty or scrapped instead.
Board page
What: one manufactured board. Where: /inventory/boards/<serial> (by serial), from the Boards page or a lot. Breadcrumb Boards › serial; title = the serial (copy) with its condition badge, its stage badge and, once tested, an EOL pass / EOL fail badge; underneath the revision (opens Boards › Revisions in a new tab), the chip id (copy; the chip’s factory MAC, bound to the serial by the station at the first flash — “not bound yet” before), the serial step — “Serialised <when> by <who> · burned into eFuse” (ESP boards; it can never change after that) or “· identity file on the Pi” (the gateway, written at imaging), with the station’s detail, or “Not serialised” — and when it was recorded. Under the breadcrumb a four-step row draws the bench sequence: Received › Flashed › Serialised › EOL passed (green = done, black = the next step, red = where it failed: a dead-on-arrival or failed flash, an EOL fail; hover a done step for when and by whom). On the right: Flash… (the primary, board.flash; “In a device: not flashed here.” or “Scrapped: not flashed.” instead when it cannot run) — on an MB board (the gateway Pi) the primary is Image a Pi… instead (?action=image; a Pi is imaged, never flashed; in a device: “image it from the unit's page”) — and the Actions menu: Run board EOL… (?action=board-eol, inventory.test, power and LED boards; greyed out until the last flash passed, in a device or scrapped) — on an MB board Run Pi EOL… in its place (?action=pi-eol, inventory.test; greyed out unless the board is Serialised, EOL failed or EOL passed, and in a device or scrapped) — Record EOL… (inventory.test), Change condition… (inventory.rma), Print sticker — a greyed-out item says why under it. Run board EOL…, Run Pi EOL… and Image a Pi… drive the station agent on this computer: see Board EOL, imaging and identity.
Overview
- Identity — Lot (its page), In device (the device it was assembled into, or “not assembled”). Print sticker opens
/inventory/boards/<id>/print. - Firmware — the last flash a station reported: release, SHA-256 (copy), when, by whom, on which station.
- End-of-line test — result, when, detail (shown as JSON when it is JSON).
- Condition — the badge with its meaning, the Stage (below), the reason when it is not OK (dead on arrival, flash failed, failed end-of-line, or the staff reason), and notes.
Stage: where the board is in the test flow
The condition is a three-way roll-up (the lot counts it); the stage says which situation it is (ruled 2026-10-17). OK means exactly one thing; Not verified and Faulty each cover several stages.
| Stage | Condition | Set by | Meaning |
|---|---|---|---|
| Received | Not verified | Receive lot | Born with its serial; not flashed yet. |
| Dead on arrival | Faulty | station flash FAIL, no chip id | No response over USB; the chip id could not be read, so nothing is bound. |
| Flash failed | Faulty | station flash FAIL with a chip id | Chip bound to the serial, but the write or the firmware's banner failed. A later good flash clears it. |
| Flashed | Not verified | station flash PASS | Running the current release; waiting for its serial step. Flashing never makes a board OK. |
| Serialised | Not verified | the station's serial step | Carries its serial: burned into eFuse on an ESP board (permanent), written into the identity file on the Pi at imaging. Waiting for its end-of-line test. |
| EOL failed | Faulty | Record EOL FAIL | An output failed under load. A later EOL pass clears it. |
| EOL passed | OK | Record EOL PASS | Every output measured good: in stock, pickable at assembly. |
A staff Change condition… (RMA, Scrapped, or OK/Faulty with a reason) changes the condition only; the stage keeps the last test-flow step. A reflash after an EOL keeps the EOL stage: the test result stands; a reflash after the serial step returns to Serialised (an eFuse burn is permanent). Boards list: a Stage column and a Filter by stage select (?stage=).
Record EOL… (?action=eol, inventory.test)
The bench result for this board: Result Pass or Fail, Detail (measurements or the failing output; free text or JSON, up to 4000 characters). Pass makes the board OK (from Not verified or a flash failure); Fail marks it Faulty (the button turns red). Greyed out until the board's last flash passed (stage Flashed, Serialised, EOL failed or EOL passed; a failed flash that bound the chip id is not enough; the backend refuses otherwise with not_flashed) and when the board already sits in a device (its test belongs to device EOL).
A board tested on the station’s EOL rig (nova-station eol board) is recorded the same way without this dialog: the rig reports the verdict to the board row (station scope inventory.test) after checking it tested the row’s revision and the chip bound to the serial (refused wrong_revision / chip_mismatch). Its ledger entry is the operator’s and carries the station, the firmware the board announced, the per-port outcomes and, for a run without operator confirmations, unconfirmed. A PASS from such a run is refused in production (unconfirmed_pass: run it again with the operator confirming each load); a FAIL is always recorded. Only a local development backend accepts it.
Change condition… (?action=condition, inventory.rma)
Pick the New condition (OK, Faulty, RMA, Scrapped — the current one is not offered; Not verified is only ever set at birth) and type a Reason (required, at least 3 characters; kept in the ledger and the audit log). Scrapped is the irreversible step and is styled red. Greyed out when the board sits in a device: handle it through the device’s RMA.
Logs
The custody ledger for this board: Board recorded, serial allocated (serial and lot), Board flashed (PASS / FAIL, release, station), End-of-line test recorded, Condition changed (from → to, reason). “By” is the operator’s email or station <name>.
Item model page
What: one entry of the models master. Where: /inventory/models/<code> (by item code, e.g. CAT6-PATCH-050 or PB/M1/RevE), from the Models page, Stock or a lot. Title = the code with status and class badges; underneath the name, the serial pattern, created and updated; Receive lot… (inventory.receive, active models) opens the Receive dialog here, preset to this model, and lands on the new lot. One page, no tabs.
- Model card (name, maker, maker part number — pencils with
inventory.receive, parts and consumables only — serial prefix or “none (counted, not serialised)”, unit, status (pencil: Retired = nothing more can be received against it); for a board model also the catalogue link, since name and status follow Boards and are edited there). - Vendor unit (
VM/M1/RevA,VM/M2/RevA; class Part with a Vendor unit badge): a Catalogue row reads “Mirrored from the catalogue (vendor unit): bought as a part, received with each unit's MAC as its vendor serial, and picked into a device through the BOM for its role.” with the link to the catalogue revision. Name and status have no pencil — under each the reason “Follows the catalogue (vendor unit): edited under Boards.” (the backend refuses them withmirrored); maker and maker part number stay editable (Blackmagic Design, the ATEM's part number). Serials areNVA-PT-…; in stock opens the OK parts of the model. Flipping the board model's Vendor unit attribute re-mirrors the revision, refused withitem_model_has_stockonce a lot of it was received. - Stock card: in stock → those boards; open lots → the Lots page filtered to this model; a note when an active board model has no open lot, because a station cannot record a new board then.
Part page
What: one serialised sourced part. Where: /inventory/parts/<serial> (by serial), from the Parts page, a lot, a scanned sticker, or a device’s assembly map. Title = the serial (copy) with its condition badge and, once inspected, an Intake pass / Intake fail badge; underneath the model (link), received, inspected and updated. On the right: Record intake… (the primary until the part is inspected, inventory.receive) and the Actions menu: Record intake again…, Change condition… (inventory.rma), Print sticker (/inventory/parts/<id>/print); in a device the items say “In a device: handle it through the device’s RMA.”
- overview — Part card (lot, vendor serial (pencil, copy), intake with date, condition with its meaning and the reason when not OK, the device it sits in or “not assembled”, notes (pencil)). For a vendor unit (the ATEM,
VM/M1/RevA) the row reads Vendor serial (MAC): required and unique, saved upper case with colons (a dash is accepted; refusalsbad_vendor_serial,vendor_serial_taken,vendor_serials_requiredunder the field with their kind), shown in red as “missing” when absent — such a part cannot be fitted (vendor_serial_missing). A Kind row says “Vendor unit”, and once it sits in a device the row reads “NVA-SB-… under roleVM”. - logs — the ledger: recorded, inspected, condition changes, built into a device.
Record intake… (?action=intake, inventory.receive)
Incoming inspection: Pass or Fail. Pass makes the part OK (from Not verified); Fail marks it Faulty (red button). Not offered once the part sits in a device.
Change condition… (?action=condition, inventory.rma)
As for boards: the new condition plus a required reason; Scrapped is the red step; not possible while the part sits in a device.
Assemble a device
What: picks tested boards, the gateway Pi, parts and consumables into one unit of a product model, which gets its serial here (NVA-TTB-00001-x, prefix = the product line). Where: /inventory/assemble, from Assemble on the ERP Devices page. Who: inventory.assemble; others are sent back to that page.
- Scan: the box at the top of the form takes a sticker and Enter — a board serial fills its role (or joins the extra boards when the config does not name the role), a part serial fills the first free piece of its BOM line (or the extras), a lot code picks the lot of its consumable line. The line under the box says what happened (“NVA-PB-00012-3 → PB · Power board”) or why not (already picked, wrong model for the role, not a free OK piece). Everything it fills can still be changed by hand below.
- Product: choose the Product model (published models only) and the Config version — released versions only (ruled 2026-10-18; the current one is preselected when it is released, else the newest released one; a model with no released version says so). Both sit in the address bar (
?model=TTB/M1&version=1). - Boards: one select per board role the config names (MB · Gateway Pi, PB · Power board, …), each listing only boards that are free and OK — the one board gate (ruled 2026-10-18): OK comes from a passed end-of-line test, or from staff by hand with a reason (the gateway Pi until the imaging station exists; the reason is on the board’s page and in its ledger) — and whose model the config accepts for that role (the hint names the accepted models). The gateway is a config role like any other (exactly one SELF-check role per version), so it is always required. Add a board of another role remains for a role the config does not name.
- Firmware check (2026-10-24, a warning — never a block): each option shows the board's last-flashed release. When the picked board's release is not the version the config certifies for its revision, a line in amber under the select says so: “LED board on 0.2.2, v7 certifies 0.2.3: it will update after the first connection.” The box fetches the certified firmware from the cloud and updates the board itself (Board firmware delivery). A version released before 2026-10-24 carries no firmware hashes, so its box cannot fetch it; the line then reads “…, but v6 was released without firmware hashes: the box cannot fetch 0.2.3. Flash 0.2.3 at the station, or build on a config version released with hashes.”
- Vendor-unit roles (ruled 2026-10-23: the ATEM is a part, not a board). A role whose accepted models are all vendor units (
VMof SB/M1, accepting VM/M1 or VM/M2) gets no board select: the Boards card ends with “VM · Video board: a vendor unit, bought as a part — picked under Parts by its BOM line, not here.” It is filled in the Parts card by the BOM's required PART line for that role, labelled VM · Video board — VM/M1/RevA (vendor unit), whose select (Part for role VM) lists the free, OK parts of that revision with their MAC (“NVA-PT-00001-6 · MAC AA:BB:…”); parts without a MAC are not offered and the hint counts them. Scanning the ATEM's sticker fills it: “NVA-PT-00001-6 → VM · Video board (VM/M1/RevA, MAC …)”. When the BOM has no such line the Parts card says so in red and Assemble stays greyed out (bom_vendor_part_missing); a role that mixes vendor units with boards of ours is named in red too (role_mixes_vendor_units). The roles count in the bottom bar covers our roles only. - Pick by BOM: the parts and consumables come pre-listed from the version’s BOM. Each part line shows one select per piece (free, OK parts of that item model only) and reads “required”, “required · covered” or “optional”; each consumable line shows the lot select (open lots of that item model; the only open lot is preselected) with the BOM quantity filled in. Optional lines may be left empty. Add part / Add consumable add extras beyond the BOM. Without a BOM the page says the version cannot be built (every released version has one).
- Notes, then Assemble in the bar at the bottom of the window, which counts “2 of 3 roles · 14 of 15 required lines covered” and names what is still missing (“Missing: PB · Power board (board), VM · Video board (part)”); the bar never hides a field — a field you tab to or press near the bottom of the window scrolls up above it first; the button is enabled once every named role has a board and every required BOM line is covered. You land on the new device’s page. Leaving with picks made asks for confirmation.
Refusals appear in a red box under the section they concern: “… has not passed its end-of-line test”, “… already sits in a device”, “… is Faulty”, “v1 does not accept PB/M2 for role PB”, “Config v1 needs a board for every role; missing: LB”, “Lot L-00007 has 3 m left, not 5”, “Required BOM lines not covered: PSU-24V (0 of 1), CAB-2X16AWG (1 of 2.1 m)” (bom_line_short), and “… is a draft; the line builds released versions only” (config_not_released). The vendor-unit refusals show inline with their kind and a copy button — under Boards role_is_vendor_unit and role_mixes_vendor_units, under Parts bom_vendor_part_missing and vendor_serial_missing; a second ATEM for the same role is role_twice. Nothing is written when any rule fails.
The ATEM in the unit: the picked part sits in the assembly map under its role (VM, kind part), not under its item code; the unit's Assembly map shows it as “VM · part · vendor unit” with the part serial and its MAC (copy button).
What assembly does: the unit is born Assembled; the picked boards and parts are marked as sitting in it (they vanish from Available); consumables are drawn from their lots; the assembly map v1 is written; every piece gets a ledger entry.
Inventory device page
What: one assembled unit as the Inventory knows it — not the same page as the IoT core’s device page (Devices), which it links to. Where: /inventory/devices/<serial> (by serial), from the ERP Devices page or right after Assemble. Title = the serial (copy) with its state badge and, once tested, an EOL pass / EOL fail badge; underneath the product model (new tab, IoT core) and config version, when and by whom it was assembled. Under the header the sequence: Assembled › Verified › Pre-registered › EOL passed › Shipped › Activated — done steps green with their time, the current step bold, a failed EOL red. Tabs: overview · logs.
One primary verb, the rest in Actions
The button on the right is always the next step of the sequence, by state and permission: Verify build… (assembled, not verified) › Pre-register… (assembled, verified) › Record device EOL… (pre-registered, no pass yet) › Ship… (pre-registered, EOL passed); in RMA it follows the repair: Install identity… (identity pending), Push config… (ADMIN; after a gateway swap, while the box has reported no config and no device map yet: opens the registry device's Push config… dialog), Install device map… (the box reports an older map), Record device EOL… (no passed EOL yet), Return to service… (all done); shipped, activated and scrapped units have none. The Actions menu holds the rest: Re-verify build…, Record device EOL… (greyed out with why: “Pre-register the unit first…” or the state it is in; the verdict also arrives from a station running nova-station eol, see the Station agent book), Image the Pi… (?action=image, board.flash: the unit's MB board with its seed and identity) and Install identity… (?action=identity, device.cert.issue) — both greyed out unless the unit is assembled or in RMA, and the first when the assembly has no MB board —, Install device map… (?action=map, device.cert.issue; in RMA only, on a box that has its identity; greyed out with why otherwise) — all three drive the station agent on this computer (see Board EOL, imaging and identity) —, Return to service… (greyed out with why while the identity or the map is pending, or the device EOL has not passed), RMA…, Scrap… (red), Serial sticker, Download Pi seed (the product_config.json of the unit’s released config version, as a file with its SHA-256 in a header; a draft version answers config_not_released — the seed only; a box refuses it without its signature, so for a Pi use nova-station seed --unit, which writes the seed and its product_config.sig), Print label. Each verb needs its permission (inventory.assemble, inventory.test, inventory.rma); verbs the role lacks are not listed.
Overview
- Unit — build verified (who, when), shipped, activated, scrapped, notes (pencil,
inventory.assemble). - IoT core — once pre-registered: the registry device (opens its Devices page), the identity fingerprint (copy; “pending” after a gateway swap), Device map — the Inventory's current version (vN: every swap writes the next) and what the box last reported (“The box runs v2 · reported 5 min ago”, “(current)”, “runs no device map”, “has not reported yet”, “Not reported yet: the box has sent no config and no device map”) —, Claim and transfer (“Allowed”, or “Refused — in for repair (RMA)…”, “… its last device EOL has not passed”, “… taken out of service (scrapped)”: what the owner app is answered), the label version; Print label and New label… in the card header. Before that: “Not pre-registered yet” with the button.
- Assembly map (beside the Unit card) — role, serial (opens the board or part), model, condition; the latest version. A vendor unit (the ATEM) sits under its role —
VM, “part · vendor unit” — with its MAC under the serial and a copy button. Correct pick… (assembled units) and Swap component… (RMA only) in its header. - End-of-line test — the whole-unit result, date and detail.
- Consumables used — lot, item, quantity; folded when the list is long.
Serial sticker (Actions menu)
Opens /inventory/devices/<serial>/print?kind=sticker: the unit’s serial as a QR with the product model and config version, printable the moment the unit is assembled and stuck inside the chassis — scanned at Verify build and at service. It is not the claim label: that QR carries the owner’s secret and prints from the IoT core card after pre-registration (?kind=label).
Verify build… (?action=verify, inventory.assemble, assembled or pre-registered units)
Scan the sticker of every board and part in the unit (one serial per line; a scanner types the serial and Enter). The backend compares the scanned serials with the latest assembly map and answers with what matched, what is in the map but was not scanned, and what was scanned but is not in the map (a wrong pick or a stray sticker). A full match stamps Build verified (who, when) on the Unit card and writes Build verified to the ledger; anything else writes Build check failed and leaves the unit unverified. Pre-register… is greyed out until the build is verified (not_verified), so no unit reaches the IoT core with a record that does not match what was fitted. Later the boards will verify themselves — each one announces its eFuse serial at power-on — and only the parts will need the scan.
Correct pick… (Assembly map card, ?action=correct, inventory.assemble, assembled units only)
The recorded piece in one role was not the one actually fitted (a wrong pick, or a damaged part swapped for a good one on the bench). Pick the role, the piece that is really in the unit (free, OK, right kind) and a reason: a new assembly-map version is written, the recorded piece goes back to stock as it was (it never left the shelf, so it does not need a re-test), and the build must be verified again. After pre-registration the same correction is an RMA swap.
Pre-register… (?action=ingest, inventory.assemble, verified assembled units only)
- The dialog shows Identity activated at the station — the certificate the station's identity job (Install identity…,
nova-station cert piorimage --unit) issued, installed on the box and activated (fingerprint with copy). Nothing to type. Without one the dialog says “No activated identity yet” with the job to run and Pre-register stays off (the backend answersidentity_not_activated). Pick the Claim policy (LOCKED, or OPEN for demo units). Pre-register. - The Inventory sends serial, product model, config version, fingerprint and a freshly generated label pair to the IoT core in one call. The dialog turns into “<serial> is registered” with the Claim code and Claim secret, each with a copy button, and a Print label button that opens the printable label with its QR. The IoT core keeps only hashes of the pair; the vault keeps it encrypted, so the label can be reprinted later from the IoT core card. The unit becomes Pre-registered.
Refusals, in the dialog's banner: identity_not_activated (“… has no activated identity yet: run the identity job at the station …”), “The IoT core refused …: UNKNOWN_CERTIFICATE — …” (the certificate is not the serial's active one), “… CONFLICT — …” (the fingerprint or serial is known with other facts), UNKNOWN_MODEL / UNKNOWN_CONFIG (publish the model or the version first), “… was pre-registered already”.
Record device EOL… (?action=eol, inventory.test)
Pass or Fail plus detail, after the whole-unit test (claimed with a factory-tester account, every channel exercised, released; on an owned unit in RMA a service test without a claim). On a Pre-registered unit, or a pre-registered unit in RMA — the item says why before pre-registration (device EOL claims the unit through the cloud) and after shipping; the backend refuses otherwise with not_ingested.
Ship… (?action=ship, inventory.assemble)
Pre-registered → Shipped (ruled 2026-10-18), with optional notes (carrier, consignment). The primary only when the last device EOL passed; the backend refuses otherwise with eol_required. The first owner claim then moves the unit to Activated. A shipped unit that comes back goes through RMA….
Scrap… (Actions menu, ?action=scrap, inventory.rma)
Final, red button with a reason: a mis-built or written-off unit. Pick what its components become — Re-test (not verified) by default, Faulty, RMA or Scrapped — every board and part leaves the unit and goes back to stock under that condition (a re-tested board’s stage goes back to Flashed, or Received when it was never flashed; a part’s intake result is cleared), the consumables are gone, the assembly map stays as history, and a pre-registered unit is revoked in the IoT core so its label can never claim it. Offered on assembled, pre-registered and RMA units; an activated unit (device_owned) or a shipped one (device_shipped) goes through release and RMA first.
Label (IoT core card, inventory.assemble)
Once pre-registered, the unit’s live label pair lives in the vault (encrypted at rest with the vault key; the IoT core holds hashes only). Print label opens /inventory/devices/<id>/print?kind=label: the label as it prints — QR of the claim URL, product, serial, claim code — with a Print button (the console chrome is left out of the print) and the facts beside it. Every read is in the audit log under your name, because the secret leaves the vault in clear: do not photograph or forward the page.
New label… (?action=new-label): refurb or lost label. A reason is required; a fresh pair is minted and vaulted, the IoT core replaces its hashes, so the old label stops working at once; the dialog shows the new pair and offers Print label. Refused with device_claimed while an owner is bound (release first) and with vault_off when no vault key is configured (then use the registry page’s New label…).
RMA… / Return to service… (inventory.rma)
RMA… (Actions menu, ?action=rma, reason) takes the unit into RMA from any live state — assembled (a mis-pick found before shipping), pre-registered, shipped (a returned unit) or activated — and remembers the state it had; the IoT core registry is not touched (revoke it there if the unit must stop working). While in RMA the IoT core refuses a claim or a transfer of the unit (not_claimable / not_transferable, reason in_rma); an owner keeps the device and may release it.
The repair sequence (ruled 2026-10-26: swap first, then one station visit)
- Swap component… on the Assembly map card (
?action=swap, RMA only; the same dialog as Correct pick, with the extra fields): pick the piece, a replacement (only free, OK pieces of the right kind are offered), what the removed piece becomes — Re-test (not verified) by default, Faulty, RMA or Scrapped, never OK: a piece that has been in service is tested again before it is built in again — and a reason. Every swap writes the next device map version and clears the device EOL. Under the fields the dialog says what follows:- The gateway (the MB board, the Pi that carries the identity) of a unit that has one: “The swap revokes the unit's identity now …”. Nothing to type: no fingerprint, and allowed on an owned unit. Every certificate of the serial is revoked at the swap and the registry identity is cleared, so the removed Pi is refused at connect (a box still connected is dropped); the owner keeps the device (shown as in repair). The unit is identity pending.
- Any other piece: “The swap writes device map vN. The box refuses the new piece until the station installs it.”
- The station visit. After a gateway swap: Install identity… (the primary while the identity is pending) with the new Pi cabled — the station checks the Pi on the cable is the unit's gateway in the Inventory (forgot the swap? it refuses in plain words, nothing issued), the box makes its key, the Device CA issues, the box installs, the cloud activates it (the unit's identity in the Inventory and the registry; the owner of a claimed device is told device.repaired) and the device map follows. A same-Pi re-key (boot medium replaced) is the same job without a swap: type the RMA reason in the dialog. After a board-only swap: Install device map… (the primary while the box reports an older map), an RMA order with its reason.
- Push config… (after a gateway swap; ADMIN). A fresh Pi has no seed, so it reports neither its config nor its device map until the config push seeds it (bench 2026-10-26). While the box has reported neither, the unit page's primary is Push config… — a link to the registry device's page with its Push config… dialog open (
/devices/<serial>?action=config-push) — and the map badge reads “not reported yet”, not an older map. Push the released config with the box's WAN connected; once it reports, the sequence goes on (the map, when it is behind; the device EOL). Without ADMIN the button is not shown and Return to service… says an admin pushes the config. - Device EOL in RMA (
nova-station eol --unitat the station; Record device EOL… is the primary meanwhile, to enter a verdict by hand): it starts only when the box runs the Inventory's identity and current map (eol_blockedotherwise: identity pending, map missing, map stale). On an owned unit it is a service test: no factory-tester claim, the owner keeps everything, the commands go as the station operator (actorservice:<operator>), a factory reset is refused (factory_reset_refused) and the owner's Activity gets “Device tested” (no push). An unowned unit is claimed by the factory tester as before. - Return to service… (the primary once all is done,
?action=return, reason) restores the remembered state — a shipped or activated unit stays shipped or activated. Greyed out with why while “Identity pending: run Install identity… at the station …”, “The box runs map v2; install v3 at the station first …”, “The box has reported no config or device map yet: push the config (Push config… on the registry device) …” (the box must report the current map: power it with its WAN connected) or “Run the device EOL first …”; the backend refuses a pre-registered unit without a passed EOL witheol_required. Claims and transfers need it back in service with the EOL passed.
Status badges beside the state in the title, amber while they need a person: Identity pending (station), Map v3 pending on the box (box runs v2) (or “box has none”, or “not reported yet” while the box has sent no config and no map — then the next step is Push config…, not the map), EOL pending (a swap cleared the device EOL; run it in RMA, then Return to service).
Logs
The ledger: Device assembled (model, version, boards), Pick corrected (role, out → in, map version), Build verified / Build check failed (matched, missing, unexpected), Pre-registered in the IoT core (outcome), Device end-of-line test recorded, Shipped (notes), Label read from the vault, New label issued, Taken into RMA / Returned to service, Component swapped (role, replacement, map version; for a gateway “identity pending: N certificates revoked at the swap”), Device map issued (version, gateway chip, roles, station), Identity activated (fingerprint, the one it replaces, station), Scrapped (pieces released, their condition, registry revoked), Activated by its first owner — the IoT core tells the Inventory in-process the moment the first owner claim happens; the state turns ACTIVATED.
Inventory refusals
What: the refusal kinds about vendor units (ruled 2026-10-23: the ATEM mixer, VM/M1 and VM/M2, is a part, not a board). Each shows in red where it happened with its kind in mono and a copy button; nothing is written.
| Kind | Where | What it means | What to do |
|---|---|---|---|
vendor_serials_required | Receive lot; part page | A vendor unit was received without MACs, or its vendor serial was cleared. | Type each unit’s MAC (one per unit); a vendor unit keeps its MAC. |
bad_vendor_serial | Receive lot; part page | A vendor serial is not a MAC. | AA:BB:CC:DD:EE:FF — six pairs of hex digits from the unit’s label. |
vendor_serials_count | Receive lot | The number of vendor serials differs from the quantity. | One per unit: fix the quantity or the list. |
vendor_serial_taken | Receive lot; part page | A part with that MAC is already received (or the MAC is typed twice). | Check the label; the same unit is never received twice. |
vendor_serials_parts_only | Receive lot | Vendor serials were sent for a board or consumable lot. | Only part lots carry vendor serials. |
mirrored | Item model page | Name or status of a mirrored model (a board revision, a vendor unit). | Edit them on the catalogue entry under Boards; maker and MPN of a vendor unit stay editable. |
item_model_has_stock | Board model › Attributes | The Vendor unit attribute changed for a revision already received as the other kind. | A different kind of item is a new model. |
bom_vendor_part_missing | Config version checklist; Assemble | The BOM has no required PART line for a vendor-unit role. | Import a BOM with the line (code VM/M1/RevA, role VM, qty 1) — on a released version that is a new config version. |
role_is_vendor_unit | Assemble | A board was named for a vendor-unit role. | Pick the received vendor part under Parts instead. |
role_mixes_vendor_units | Assemble | A role accepts vendor units and boards of ours. | Split the role in a new config version: a role is one kind of item. |
vendor_serial_missing | Assemble | The picked vendor part has no MAC recorded. | Record its vendor serial on the part page first. |
role_twice | Assemble | Two parts picked for one vendor-unit role (or a role listed twice). | One ATEM per unit. |
Stations
What: a station is a machine (a bench Mac, a line jig) that runs the station agent and flashes released firmware onto boards. Each console works with its own agent flavour: nova-station-dev (dev), nova-station-qa (QA) or nova-station (production); the console refuses a sign-in from another flavour (agent_flavour_mismatch). Each station has its own key. The key only says which machine is talking; to flash, a person signs in on it (see Station sign-in), and every flash is recorded under that person.
logout), and lock the computer. The 30-minute idle sign-out and an admin’s Sign out are a safety net, not a substitute.Where: /stations, sidebar IoT core › Stations, two tabs in the URL: Stations (this section) and Agents (every uploaded build of the agent program); Set up this computer… in the header opens the install panel for the machine you are on — see Agent installers. Who: every staff role sees both tabs and the set-up dialog; only ADMIN sees New station… and Upload build….
The list
Paged (rule 14), with a search box (name, notes or key prefix) and a status filter: Active by default; choose Revoked or Active and revoked to fetch revoked stations (?status=REVOKED|ALL). Sort by Name, Last used or Created. Columns: Name (opens the station page; notes underneath), Status (Active green, Revoked red; hover for what it means), Agent (the flavour badge Dev / QA / Prod and the version the agent last called with — every call carries them; “not reported” until an agent 1.0.0 or later has called), Key prefix (the first characters of the key, so you can tell which key a machine holds without ever seeing the key again), Active sessions (people signed in right now; the number opens the Sessions tab), Last used and Created.
With no stations the page says “No stations yet” and explains that the next step is to create one and install the agent on that machine.
New station… (ADMIN, ?action=new)
- Click New station…. Type a Name — where the machine is, e.g. “Bench Mac”, “Line 1 jig” (1–60 characters, unique) — and optional Notes. Click Create station.
- The dialog turns into “<name> is ready” with the warning “Copy the key now — you will not see it again”. It shows the Station key with a copy button, then install first (from Stations › Set up this computer… opened on that machine): macOS — download
<program>-<version>-macos-arm64.pkgand open it (not signed yet: the first time, System Settings › Privacy & Security › Open Anyway), enter the administrator password once; it installs the program on the PATH and the writer helper; Windows — run the setup program as administrator; the setup installs the program on the PATH and the writer helper service. Under “Then, in a terminal on that machine” the commands an operator types, each copyable, the key step carrying this key and all of them named for this console's flavour — on the dev console:
nova-station-dev config set console_url http://<backend>:8080
nova-station-dev config set console_origin <this console>
nova-station-dev config set station_key <key>
nova-station-dev login
nova-station-dev autostart on
nova-station-dev flash --revision PB/M1/RevE
nova-station-dev logoutThe QA console shows the same with nova-station-qa, without the two console settings (built in). The production console shows nova-station config set station_key <key>, nova-station login (or sign in from any station dialog), nova-station autostart on and nova-station doctor: the production build has no job commands — every job starts from the console — and you sign out in the console's station panel.
- Click Done: you land on the new station’s page.
Nova keeps only a hash of the key. If it is lost, revoke the station and create a new one. Errors under Name: a blank or too-long name, or “A station called … already exists”.
A new station starts allowing firmware.read and board.flash; change that on the station page (Allowed on this station).
When: once per machine, before its first flash; again only to replace a lost or leaked key (create new, revoke old).
Flash from the console
What: the Flash… button writes the current release of a board revision onto a board plugged into this computer, without a terminal. The console cannot touch USB itself: the nova-station agent running on the computer does the work and the page drives it. Where: the ERP board page (ERP › Boards › a board, serial prefilled from its sticker) and the catalogue revision page (IoT core › Boards › Revisions › a revision, type or scan the serial). Who: anyone with the board.flash permission; the agent checks it again at the station.
What the dialog shows first
Before the form, the dialog looks for the agent on your computer and shows one of four states:
- No agent answers — the install panel for this console's flavour (“nova-station-dev is not responding on this computer”; the console looks on that flavour's own port: 8765 dev, 8766 QA, 8767 production): the installer for your system (detected; correct it if needed), its SHA-256, the steps (save it; set this machine's station key — on dev also the backend address and this console's address, QA and production have theirs built in;
<program> autostart on, which makes the computer run the service at every login). The page keeps looking every few seconds and continues by itself once the agent runs. The panel reads "not responding": not installed yet → download and follow the steps; already installed → just switch it on (autostart on, orservefor one session). A page cannot look at what is installed, it only sees whether the service answers; if this browser has seen an agent answer here before, the panel says so and folds the download under Reinstall or update. If no installer is published for your system, it says so and names Stations › Agents. - Another flavour answers — “This is nova-station-qa, not nova-station-dev” (or, for an agent before 1.0.0, “The agent on this computer predates the flavours”) with the install panel: install this console's flavour; another flavour may stay installed next to it (each has its own settings folder and port). An agent before 1.0.0 holds port 8765 itself: the panel says to switch it off first (
nova-station autostart off), then installnova-station-devand set the station key again (its settings live in~/.nova/station-dev/). - The console does not know its environment — the backend did not send it (it predates the environments change): “This console does not know its environment”, and nothing is probed or offered rather than guessing a flavour.
- Nobody is signed in on it — one button, Sign in on this station as <you>. The agent starts a sign-in, the console approves it in your name (you are already signed in here) and the agent picks it up: three seconds, no code to type. Refused if you have no permission that the station allows, or if the station's key was revoked. When the console ended your previous sign-in (agent 0.6.6 and later report it), the panel says so above the same button: "Your sign-in on <station> ended — no station activity for 30 min (at 14:35)", "— the 12-hour limit" or "— signed out (by you or an admin)".
- Someone else is signed in on it — their name and a single button, Sign <name> out. Their session ends on the backend (recorded on the station as “replaced at the console by you”) before anything else; then the sign-in button appears. So nobody flashes in a colleague's name because they left a station signed in.
- Signed in as you — the form. The line reads "as you on <station> · nova-station-dev <version> · until 18:40 · signs out at 14:35 if the station stays idle (30 min without activity)": the 12-hour end and, from agent 0.6.6, when the console will end it for inactivity; a running job counts as activity, so the agent keeps the session alive while it runs (an older agent shows only its own copy of the 12-hour end). A Sign out link ends your agent session; a note appears when a newer agent is published.
The agent asks the console about the session every time the page checks on it (every 20 seconds), so what the panel shows is the console's answer. If the agent could not reach the console just now, a small note says "Couldn't confirm the sign-in with the console just now" and the last known state stands; nothing is blocked. The form is only shown for a live sign-in. If a run is stopped because the sign-in ended mid-run, the dialog says "Your sign-in on this station ended" with Sign in again as <you>; then start the run again.
The form and the job
Revision (fixed), Serial (the sticker's serial; prefilled on the board page, scanned or typed on the revision page, or left empty to be asked during the job), USB port (Auto-detect finds the one board plugged in) and Notes. Start flashing runs the same job as nova-station flash: the log scrolls in the dialog; a question from the agent (hold BOOT, retry, no chip id read → Record dead on arrival or Skip) appears as buttons, a scan request as a text field. The end shows PASS or FAIL with the reason, and the board row updates (chip id bound at the first flash, firmware, condition). Closing the dialog while a job runs asks first.
Safety: the agent listens on the computer itself only and accepts calls from this console's address with a header a stray web page cannot send. Who may act is the signed-in operator, never a pairing code. Signing out of the console does not sign out the agent; the 30-minute idle limit (no station activity), the 12-hour limit and Sign out do.
Board EOL, imaging and identity
What: five more dialogs that drive the nova-station agent on this computer, like Flash…: Run board EOL… (the board on the EOL rig), Run Pi EOL… (an imaged master board on its board LAN), Image a Pi… / Image the Pi… (a gateway Pi's card or NVMe), Install identity… (a unit's identity certificate and device map) and Install device map… (the map alone, after a board-only RMA swap). They open with the same four agent states as Flash (no agent, nobody signed in, someone else, you) and need agent 0.5.0 or later (Run Pi EOL… 0.5.3); an older one shows “The agent on this computer is too old” with the installer. One board EOL, one imaging job and one Pi EOL run at a time per station; a second start says which is running.
Run board EOL… (ERP board page, Actions; inventory.test)
- Form: Serial and Revision (fixed), then Before you start: this computer is the board's gateway for the system the agent runs on (macOS, Linux or Windows). What: a dedicated Ethernet port on this computer (a USB-Ethernet adapter) cabled straight to the board hands out its address and is its router. Why: the board dials its gateway on port 9000, where the agent listens; without it the board gets no address and the run waits forever. How: macOS — System Settings › General › Sharing › Internet Sharing from Wi-Fi to the USB-Ethernet adapter, and allow incoming connections for
nova-station; Linux — NetworkManager "Shared to other computers" or a static10.42.0.1/24plus dnsmasq (copyable commands); Windows — Internet Connection Sharing onto the adapter (it becomes192.168.137.1); the firewall ruleNova Station board EOL (TCP 9000)comes with the setup program, and if Windows still asks aboutnova-stationchoose Allow, never Cancel. Then the address the board dials, "one board at a time", and a copyable command that shows what holds port 9000. Rig: the harness with a load on every output, Ethernet, then the 24 V supply last (loads are judged on 24 V, never USB power). Afterwards: turn Internet Sharing off again and re-cable before imaging or testing a box — the box's Pi is the gateway on its own board LAN. Below it: Ask the meter reading at every switched connector, Bus evidence only (no questions; the run is marked unconfirmed), Notes, Start board EOL; power-cycle the board after starting. - Run: until the board dials in, “Waiting for the board to dial this computer… power-cycle it now”, with what to check after two minutes (sharing on for the rig adapter, port 9000 free). Then a table with one row per port — Port, Connector, Load, Result: Waiting, Testing (highlighted), Pass, Fail with the failing check, Skipped; a measured voltage beside it. For the port under test an amber box asks “Port C (J7): did the load do it?” with what to look for, and Yes / No / Skip / Redo (a skipped port fails the board; Redo, agent 0.5.4 or later, “runs this output's commands again so you can watch the load” and asks again — any number of times, the port's row then says how many runs, and the verdict uses the last); with the meter, a Volts field with Submit, Skip and Redo. The agent's log is folded under Agent log.
- Result: Pass / Fail, “n of m ports passed”, why it failed, and where it was recorded: on the board row (the agent reports it, the row moves to EOL passed / EOL failed), or — offline, or the backend refused it (the reason is in the log) — “Not recorded centrally” with the result file on the station; then run it again or enter the verdict with Record EOL…. The page refreshes when the run ends.
Run Pi EOL… (MB board page, Actions; inventory.test)
- Where: ERP › Boards › an MB board › Actions (
?action=pi-eol); on MB boards it takes the place of Run board EOL…. Greyed out with the reason in a device (“its test is the device EOL”), scrapped, or before the serial step (“Image the Pi with this serial first”). - Form: Serial and Revision (fixed); Before you start: this computer cabled to the box's board LAN (the Pi's Ethernet port or the board switch; the box gives it a
10.42.0.1xxaddress), Internet Sharing off on that adapter (here the Pi is the gateway), the box powered and imaged with this serial, its WAN port in a network with DHCP. Slot drill (reboots the box twice), off by default (a trial boot of the other slot and back, about 2–5 min, only when every other check passed); Notes; Start Pi EOL. An agent older than 0.5.3 is named in a red box and Start Pi EOL stays off. - Run: the steps Board row › Model profile › Reach the box › Checks › (Slot drill ›) Record › Verdict, a line saying what is happening, then a table with one row per check — Check, Result (Pass / Fail), Detail (what the box reported). The agent's log is folded under Agent log.
- Result: Pass / Fail, “n of m checks passed”, and “Recorded on the board row: EOL passed / EOL failed”, or “Not recorded” with the result file on the station (the board stays untested and assembly refuses it). A run that stopped early — another box on the cable, the box not answering, the board not serialised — shows “Why it failed” and records nothing. The page refreshes when the run ends.
Image a Pi… (MB board page, primary; Image the Pi… on the unit page; board.flash)
- Form: Image (the MB revision; its current release is written: a sealed image that carries its own first boot, so nothing is staged on the card), the MB board and, from the unit page, the Unit (its identity installed after the boot; its seed is then sent with Push config on the unit page, once the box is online). Target disk: the disks the agent sees, with Refresh — writable ones as choices (device, size, name, bus; “Mounted now: …” when they are), refused ones greyed with “Refused: … is the disk this computer runs from / an internal disk / a partition / larger than …”. Choose one, then type its name in “Type disk4 to confirm” (Windows lists
\\.\PhysicalDrive2and asks forPhysicalDrive2); Erase and write (red) enables only when it matches, and the agent checks it again. Wait for the first boot (on by default) follows the box through self-test, board row and identity; off = write and verify only. - If the computer cannot run
ddwithout a password, the form says “This computer cannot write a disk from the console yet” and Erase and write stays off with a link, First-time setup, step 1, that opens Set up this computer… in a new tab at that section (the same step is in the Station agent book, First-time setup of a station machine). - Run: the steps Release › Target › Write › Verify › Eject › First boot › Self-test › Board row › Identity (the last two when they apply), the bytes downloaded or written (a bar when the size is known), and at First boot an amber box: “Move the card to the Pi and power the box on” with Continue / Abort. The log below.
- Result: what was written to which disk, the SHA-256 on the card (copy), the Pi's serial and the identity fingerprint (copy) when they apply.
Install identity… (unit page, Actions — the primary in RMA while the identity is pending; device.cert.issue)
For a box imaged earlier, or a Pi swapped in RMA: Unit (fixed), in RMA an optional RMA reason (re-key only) (a box that already has an identity takes the job only as an RMA order: a re-key of the same Pi), a reminder to cable this computer to the box's board LAN, Install identity. Agent 0.8.0 or later (an older one is named and the button stays off: it would not activate the identity, and the backend then refuses pre-registration and device EOL). The station first checks that the Pi on the cable is the unit's gateway in the Inventory (a swapped Pi needs Swap component… first; nothing is issued otherwise), then: the box makes its key, the Device CA issues (only for the Pi the Inventory knows), the box installs it, the cloud activates it, the unit's device map is signed and installed, and the box shows both back. Steps Operator › Reach the box › Issue, install, activate, map; the result is “Identity installed and activated” with the fingerprint (copy), its expiry and “device map vN in force on the box”. Pre-register… then shows it as the activated identity.
Install device map… (unit page, Actions — the primary in RMA while the box runs an older map; device.cert.issue)
After a board-only swap (LED or power board, the mixer): the unit's signed device map onto the box cabled to this computer, so it serves the swapped board. In RMA only, on a box that has its identity (after a gateway swap the identity job brings the map). Unit (fixed), RMA reason (required: the box is identified, so it takes the map only as an RMA order), the board-LAN reminder, Install device map; agent 0.8.0 or later. Steps Operator › Reach the box › Install the map; the result is “Device map installed — v3 is in force on the box. Next: the device EOL, then Return to service.” The same as nova-station map install --unit … --rma ….
A run that is not finished (agent 0.5.6)
- Re-attach: every station dialog (these four and Flash…) first asks the agent what is running (“Looking for a run in progress on this station…”). A run of its kind for this serial — the tab was closed, the page refreshed, you signed out of the console and back in — is picked up where it is: “Re-attached to the run in progress on this station (started hh:mm)”, its question answerable. The dialog's address (
?action=board-eol…) survives a refresh, so the same dialog reopens by itself. - Another run on the station: the form lists it above the fields — “A board EOL run on NVA-… is in progress on this station (waiting for an answer)” — with Open it (that board's page with its dialog) and Cancel that run… (asks first). A start the station refuses because the rig or the card reader is busy shows the same instead of an error.
- Cancel run: under a running job; asks “Cancel this run? It stops at its next safe point; nothing is recorded on the board row.” (Keep running / Cancel run). Greyed with “Not now: …” while the station cannot cut it — a flash or a card being written (a half-written flash or card would not boot), the Pi slot drill, the verdict being recorded. After the ask: “Cancelling: the run stops at its next safe point…”; then Cancelled and “Run cancelled” with what was (not) recorded.
- Closing a dialog whose run goes on (×, Escape, Cancel, the backdrop) asks: “Close this dialog? The run keeps waiting on the station for your answer — it cancels itself after 15 min without one.” (or “keeps going”) with Stay here, Leave it running, Cancel it. A browser refresh is not blocked: the run lives on the station.
- Two windows on one run: the second answer to the same question shows “Answered in another window: this view shows where the run is now.”
- Kept on this station: when the verdict could not be recorded (backend down, the station's sign-in ended mid-run) the result says so — “Kept on this station … will be recorded when … is signed in and the console answers”; the station sends it by itself.
- Lost: “The station restarted during the … run on NVA-…; that run was lost and nothing was recorded for it. Start it again.” — once, in the dialog or on the page.
- The board and unit pages (ERP) show, under the step row, “A board EOL run for NVA-… is in progress on the station, waiting for an answer.” with Open it while no dialog is open — only in a browser where the station agent answered before.
Agent installers
What: the station-agent builds staff download, one per system and chip, of this console's flavour only (ruled 2026-10-26): the dev console serves nova-station-dev, QA nova-station-qa, production nova-station — the program name is the flavour, the version (from 1.0.0) is not. Installers are offered for exactly two systems: macOS Apple silicon (the installer package <program>-<version>-macos-arm64.pkg; ruled 2026-10-26, the bare one-file program is no longer served) and Windows x64 (the setup program <program>-<version>-windows-x86_64-setup.exe); macOS Intel, Linux (x86_64 and ARM) and Windows ARM are listed as “not available yet”. Where: the Agents tab of the Stations page (/stations?tab=agents) and the Set up this computer… dialog (/stations?action=setup; the old Installer tab and /stations/agent land there). Who: every staff role sees both and downloads; ADMIN uploads and chooses the current build.
Agents tab
Every uploaded build, paged (rule 14): search by version, file or notes; filters by System, Chip and Current only (the build offered per system and chip, which the agent's update check names); sort by System, Version or Uploaded. Columns: system · chip, version (current badge, notes), file, size, SHA-256 (copy), uploaded, Download; ADMIN: Make current… (a confirm) on a build that is not. Upload build… sits in the page header on this tab.
Set up this computer… (dialog)
The same install panel the Flash dialog shows: Your computer detected from your browser (correct it if needed; the list holds macOS Apple silicon and Windows x64, then the others marked “(not available yet)”). For an available one: Download nova-station-dev <version> (this console's program; a 10-minute link to the file in Nova's private bucket), the stored file name, SHA-256 and the steps — macOS: download the .pkg and open it (unsigned for now: the first time, System Settings › Privacy & Security › Open Anyway); click through and enter the administrator password once — it installs <program> to /usr/local/bin (on the PATH) and the writer helper with its sudoers rule, no terminal step and no checkout; then in a terminal set the station key — on dev also console_url and console_origin, QA and production name their built-in console instead —; <program> login; <program> autostart on, with its settings folder and port, and the check (doctor, targets → writer helper: ready); Windows: download the setup program, run it and accept the administrator prompt (SmartScreen: More info › Run anyway; it installs the agent on the PATH, the writer helper service, the firewall rule for the rig and the CH343 driver, and when its Console page is filled sets the console and switches the service on), the same config set … and autostart on in a normal PowerShell if that page was left empty, then the check (nova-station doctor, nova-station targets → writer helper: ready). For one not available: “<computer>: not available yet”, naming the two that are, with no download and no steps. Below them, First-time setup (Pi-imaging stations) (anchor #first-time-setup) for the selected system: on macOS nothing to install by hand — it says the .pkg already installed the writer helper and its sudoers rule, how to check it (sudo -n -l, nova-station targets; NOT READY → open the package again), the direct-port note and, for developers who installed with pipx from a checkout, the script to run instead; on Linux the one step, install the writer helper (sudo sh ~/Arduino/nova_boards_firmware/station/installers/install-helper.sh: the root-owned /usr/local/libexec/nova-station-write and the one sudoers rule that lets the station user run it without a password; it replaces the old passwordless dd rule, which was effectively full root), as a copyable command with why and how to check it worked (sudo -n -l lists the rule, nova-station targets says the helper is ready), plus the note to plug the drive into a port directly on the computer, not a hub; on Windows nothing is installed by hand: it says the setup program already installed the writer helper service for the Windows user who ran it, how to check it (nova-station targets → writer helper: ready; NOT READY → run the setup again as this user), the direct-port note, “After a write: Cancel, never Format” (Windows offers to format the Pi's Linux partition) and Internet Connection Sharing for the board EOL rig (192.168.137.1:9000). The macOS package runs that same script as its postinstall. The Image a Pi… dialog's "cannot write a disk from the console yet" warning (agent 0.5.9+ reports helper_ready: false; the warning says how to install it — macOS: open the .pkg again (pipx installs: the script); Windows: run the setup again as this user, as administrator; Linux: the script — and the agent's own helper_problem text, and Erase and write stays disabled) links here in a new tab (/stations?action=setup#first-time-setup). Full text: Station agent › First-time setup of a station machine.
Upload build… (ADMIN, Agents tab, ?action=upload)
The dialog is titled for this console's program (“Upload a nova-station-dev build”). Pick the file that station/installers/build.sh --flavour dev|qa|prod produced on a machine of that system and chip (macOS: the installer package <program>-<version>-macos-arm64.pkg from build.sh --pkg; Windows: <program>-<version>-windows-x86_64-setup.exe; picking it fills system, chip and version); a bare macOS program (no .pkg) is refused under File (“A macOS build is the installer package … .pkg (build.sh --pkg); the bare program is no longer served.”), and a file of another flavour is refused under File (“This is a nova-station-qa build; this console holds nova-station-dev builds only.”) and Upload stays off. The upload carries the flavour (flavour) and the backend stores it with the build. Version must be the agent's own (--version, x.y.z, no flavour in it); the same version for the same system and chip cannot be uploaded twice. Make it the current build is on by default. Up to 32 MB; the SHA-256 is computed on upload and the file is stored in Nova's private bucket (checked against that SHA-256 on the way in).
Updates: nothing updates itself. The agent asks the console for the current version of its flavour for its machine; doctor and the Flash dialog say when a newer one exists (by SemVer). Replacing the file keeps the settings, station key, sign-in and cache.
Permissions (scopes)
What: a scope is one named thing a person may do beyond their role: on a station, or in the Inventory. The list is fixed by Nova (a closed catalogue); the console always shows the current one, each scope with what it allows. Today:
| Scope | Allows |
|---|---|
| firmware.read | Read board revisions, download their released firmware and a unit's released Pi seed |
| device.test | Station: run the device EOL — claim the unit as the factory tester, drive every channel, record the verdict |
| device.cert.issue | Station: issue a unit's identity certificate — the Device CA signs the Pi's CSR at assembly or for a replacement gateway |
| board.flash | Flash boards and record the result |
| inventory.write | Station: write board and part rows to the Inventory (serial allocation, flash results) |
| inventory.read | Console: see the Inventory (models, lots, boards, stock) |
| inventory.receive | Console: receive lots, author part / consumable models, record part intake |
| inventory.test | Record board and device end-of-line results: in the console, and a station's board EOL rig |
| pi.image | Station: write a gateway Pi's card from the current Pi image release of its MB revision |
| inventory.assemble | Console: assemble, verify, pre-register and ship units; print labels |
| inventory.rma | Console: RMA / refurb / scrap and condition changes |
More will be added as stations take on more work (for example board end-of-line tests); they appear in the console by themselves. The inventory.* permissions are checked by this console directly (an ADMIN holds every one); the station rule below applies to the station scopes.
Person × station
Two lists decide what a signed-in operator may do, and both must contain the scope:
- The person’s grants — Users › person › Permissions. New SUPPORT accounts have none.
- The station’s allowed scopes — Stations › station › Allowed on this station.
The operator may do what is in both (for example: granted firmware.read + board.flash on a station that allows only firmware.read → may download firmware there but not flash). This is checked on every command, not only at sign-in: removing a grant or narrowing a station bites on the operator’s next command, without signing them out.
Who changes them: ADMIN only, on those two pages. Every change is in the audit log (the person’s or the station’s Activity tab) with what was added and removed. Anyone can read their own grants on Account.
What people see when a scope is missing
- At sign-in: if the person and the station have no scope in common, the approval page says “Your account has no station permissions that this station allows. Ask an admin to grant them on your user page (Permissions) or to allow them on this station.” and Approve is greyed out. Otherwise it lists what they will be able to do before they approve.
- On the station: a command needing a scope the operator does not have now is refused with
scope_missing; the agent prints “Not allowed: board.flash … Ask an admin: Users › your account › Permissions, or Stations › <name> › Allowed on this station”. Fix it on either page, then rerun the command — no new sign-in needed.
Station page
Where: /stations/<id>. Breadcrumb Stations › name. Header: the name with its status badge; key prefix, “N operators signed in”, last used, created, and when and why it was revoked; Revoke… (ADMIN, active stations only). Tabs: overview · sessions · logs. Every staff role can read it; the verbs that change things are ADMIN-only and hidden for SUPPORT.
logout), and lock the computer. The 30-minute idle sign-out and an admin’s Sign out are a safety net, not a substitute.Overview
- Station card: Id, Key prefix (“The key starts with this. The full key was shown once, when the station was created.”), Agent (flavour badge and version, with the program name, as the agent last called; “not reported” before an agent 1.0.0 has called), Active sessions (→ Sessions tab), Notes (pencil, ADMIN, up to 1000 characters), Created by.
- Allowed on this station: the scopes that may be used on this machine, each with what it allows. An operator gets these, limited to their own station permissions (see Station permissions). With none it warns “Nothing may be done on this station; sign-ins will be refused.” Edit allowed (ADMIN,
?action=edit-scopes; greyed out on a revoked station) shows a checkbox per catalogue scope; Save applies at once, also to operators already signed in. When: e.g. a machine that should only fetch firmware, never flash.
The terminal commands an operator types are shown once, with the key, when the station is created (New station…), and in the Station agent book.
Sessions (?tab=sessions)
Who signed in on this station, newest first (the latest 100). A session lasts 12 hours, or 30 minutes without activity. Columns: State (active / ended), Operator (name — a link to their staff page for admins — and email), Started, Last seen, Expires / ended (for ended sessions: when, and why: “signed out”, “timed out”, the reason an admin gave, or “station revoked: …”). Sign out (ADMIN, active rows) opens a dialog that needs a Reason; the person can no longer flash there until they sign in again.
Logs (?tab=logs)
The audit log, newest first, paged: created, notes updated, “Changed what is allowed on the station” (with + added / − removed scopes), revoked, sign-in approved / denied, signed in / out on the station, operator signed out by an admin, and every flash. A flash reads “Flashed PB/M1/RevE” (or “Flash failed on …”) by the operator, with a PASS/FAIL badge, the release, the start of the SHA-256, the port, MAC address, the version the board reported, the seconds it took, the operator’s notes and any error. The raw record stays under “Details”.
Revoke (ADMIN, ?action=revoke)
The dialog says: “The key stops working at once, all N signed-in operators are signed out and any pending sign-in is refused. This cannot be undone: to use the machine again, create a new station and set its key.” A Reason is required (recorded in the audit log); the confirm button is red. When: a machine is retired, lost, or its key may have leaked.
Station sign-in (approve a code)
What: how a person signs in on a station. On the station, the agent's login (nova-station-dev login on the dev console, nova-station-qa login on QA, nova-station login on production) prints a code and a link like <console>/stations/login?code=ABCD-EFGH. Open the link (on any device) signed in to this console and approve: you become the operator of that station for 12 hours, or 30 minutes without activity (or until you sign out). Every staff role may approve, if they have station permissions this station allows (see Station permissions); you can only sign yourself in.
logout), and lock the computer. The 30-minute idle sign-out and an admin’s Sign out are a safety net, not a substitute.Where: /stations/login?code=…. If you are not signed in, the console asks you to sign in first and then brings you back to the same link. Opened without a code, the page shows a Code field (“The eight characters nova-station-dev login printed”, named for this console's flavour) and Continue.
Approving
- The page shows the Station name and the code in large letters: “Check this code matches the one on the station’s screen.” If it does not match, click Deny — someone else started that sign-in.
- It shows when the code expires (codes last 10 minutes).
- Under You will be able to: it lists what you may do there — your station permissions that this station also allows, each with what it allows. If there are none, it says instead “Your account has no station permissions that this station allows. Ask an admin to grant them on your user page (Permissions) or to allow them on this station.” and Approve is greyed out (hover it for why); Deny still works.
- Click Approve — sign in as <your name>. The page says “You are signed in on <station>” and to return to the terminal, which finishes signing in by itself within a few seconds. Under the button and again on the success screen the page reminds you: “Remember to sign out when you leave the station.”
When it does not work
| The page says | What to do |
|---|---|
| No sign-in with this code | Check the code on the station’s screen (the field accepts it with or without the dash), or run the agent’s login command again. |
| This code has expired | Run the agent’s login command on the station again for a new code. |
| Already approved / Already used | Nothing: the station is (being) signed in. |
| Denied | Someone refused it. Run the agent’s login command again if it was yours. |
| This station has been revoked | Its key no longer works. Ask an admin to create a new station for the machine. |
| Your account has no station permissions that this station allows… | Ask an admin to tick what you need on Users › you › Permissions, or to allow it on Stations › station › Allowed on this station; then open the link again (the same code works while it lasts). The same message appears if a permission was removed between opening the page and pressing Approve. |
This documentation page
Where: /docs. Who: anyone — it is public and needs no sign-in, so partners can read the API reference and new staff can read this guide before they have an account. The “Staff console →” link in the top-right corner goes to sign-in.
- Two panes: the grouped index on the left (on a phone, behind the menu button in the top-left corner) and the content on the right. The index highlights the section you are reading as you scroll; clicking an entry jumps to it, and the address bar carries the section (
/docs#console-devices) so you can link straight to it. - Code samples have a Copy button in their top-right corner.
- The version line under the title (“v1 · date”) is the API version and the date of the last content change.
- The index has four books: Partner API (for integrators), Console application (this staff console, page by page), Mobile app and Station agent (flashing boards, building and publishing releases). Content lives in the console’s source (
lib/docs/content.tsx,lib/docs/console.tsx,lib/docs/station.tsx). Rule: every new or changed console page updates its section here in the same change, so what you read matches what you see.
The app at a glance
What: the Nova app is the owner app for Nova boxes. You use it to add a box to your account, switch and dim what is connected to it, save scenes, hand a box to someone else, see what happened, and manage your account. It is an Android app. There is no iPhone app.
Who: anyone who owns a Nova box. Staff use the web console instead (Console application book). Partners use the API (Partner API book), with keys an owner creates in this app.
The four tabs
After you sign in, a bar at the bottom of the screen holds the tabs. It shows only on these tab screens; pages you open from a tab have a back arrow instead.
| Tab | What you do there |
|---|---|
| Devices (start) | Your boxes, plus the Moments and Transfers sub-tabs. Top-right: Redeem a transfer code (arrow-into-box icon) and Add device (+). |
| Activity | Everything that happened to your account and devices, newest first, with a Needs attention block on top. A number on the tab icon counts unread rows. |
| Developer | Only when Nova has granted you developer access and you switched it on under Account. API keys, webhook, the emergency stop. |
| Account | Your name and email, password, this phone, the developer access row, Help (tips and this guide), the app version, Sign out. |
The Android Back button on any tab other than Devices takes you to Devices. Back on Devices leaves the app.
What talks to the Nova cloud, and what stays on the phone
- Everything you see and change goes through the Nova cloud. Your devices, names, scenes, moments, transfers, the activity feed and your developer settings live there, so a new phone shows the same things after you sign in.
- Control goes through the cloud too. While the app is open it keeps one live connection to Nova; switches and dials travel over it and the screen shows what the box actually reports back. The connection closes when the app goes to the background. The one exception is Bluetooth: when a box is offline (or you pressed its button) and you stand next to it, the app can reach it directly to switch channels and set up Wi-Fi (see Reach a box over Bluetooth).
- On the phone: your sign-in (so you stay signed in), the phone’s name for this sign-in (model and Android version), and the permissions you grant (camera for scanning, notifications for pushes, Nearby devices for Bluetooth), a sign-in key that never leaves the phone, a 14-day offline pass per box so Bluetooth works without internet, and a copy of what the app shows (your devices, their scenes and kiosk settings, moments, transfers, the last 200 or 30 days of activity, stream channels without keys) so it opens and shows your boxes without internet; when it is back, only what changed is fetched. Signing out deletes the copy. Copied codes and keys sit on the phone’s clipboard.
Things that work the same everywhere
- A problem with one field shows under that field. Any other problem shows in red text with a small grey
code: …line under it. Quote that code when you ask Nova for help.network_errormeans the phone cannot reach Nova. - Buttons stay visible but greyed out while you cannot use them (nothing to save yet, the box is offline, a request is running).
- Anything that hands over or wipes property asks first: a dialog, your password, or a button you must press and hold.
- After a finished flow, a short message appears at the bottom of the screen (“Password changed”, “Device released…”).
Install and first launch
What: getting the app onto your phone and opening it for the first time.
When: before you can add your first box. You need an Android phone running Android 8.0 or newer.
How
- Install the Nova app on your phone. There is no Play Store listing yet; Nova gives you the installer.
- Open Nova. A spinner shows briefly while the app checks whether you are still signed in.
- If you are not signed in, the sign-in screen opens (“Nova · Sign in to your account”). New here? Tap Create account.
- If you were signed in before, the app opens straight on Devices.
Create an account
What: a Nova account for one person, identified by email and password. Where: sign-in screen › Create account. When: once, before your first box.
How
- Fill in Display name, Email, Password and Confirm password. Your display name is how Nova addresses you; nobody else sees it.
- Under the password a checklist ticks off as you type. All six must be ticked:
- 12–128 characters
- No leading or trailing spaces
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one symbol
- Under Confirm password the app says whether the two passwords match.
- Tap Create account. It stays greyed out until every field is filled, the email contains @, the checklist is complete and the passwords match.
- Nova emails you a 6-digit code and the app moves to Verify email (next section).
If the email is already in use, or the server rejects a field, the reason appears under that field.
Verify your email
What: proves the email address is yours. You cannot sign in or add devices until it is verified. Where: opens by itself after Create account, and again whenever you try to sign in with an unverified email (the app sends a fresh code and opens this screen).
How
- The screen says “Check your inbox” and names the address the code went to.
- Type the code in 6-digit code (digits only; the field stops at six).
- Tap Verify.
- You land back on the sign-in screen with a blue banner “Email verified. Sign in to continue.” and your email already filled in.
- No email? Wait for the countdown (“Resend in 30s”), then tap Resend code. The app confirms “A new code was sent.” Only the newest code works.
- Too many wrong tries (
otp_exhausted): the app says “Request a new code below.” Use Resend code. - Typo in the email? Tap Wrong email? Back to return to the sign-in screen, then start again with the right address.
Sign in, one phone at a time, sign out
What: signing in to your account on this phone. Where: the first screen whenever you are signed out.
How to sign in
- Type your Email and Password. The eye icon in the password field shows or hides what you typed.
- Tap Sign in. You land on Devices.
Wrong email or password: a red message with its code appears under the fields. Forgot it: tap Forgot password? (next section).
One signed-in phone per account
Your account can be signed in on one phone at a time. Signing in on a new phone signs the old one out.
- On the new phone, a message at the bottom says “Signed out other device(s)”.
- On the old phone, the next thing it asks Nova for fails, the app drops back to the sign-in screen and a red banner says why. The same happens if your session expires, Nova support signs you out, or your account is disabled (codes
session_evicted,session_expired,session_revoked,account_disabled).
You can see which phone this sign-in belongs to under Account › Security › This device (for example “Pixel 8 · Android 15”).
Sign out
Account › Sign out (the red button at the bottom). The app stops push notifications to this phone, ends the session at Nova and returns to the sign-in screen. There is no confirmation dialog. This phone also forgets the box identities it remembered for checking streaming keys; the next Go live checks them again. Your devices, scenes and settings stay in your account.
Forgot password
What: sets a new password using a code sent to your email. Where: sign-in screen › Forgot password? (the email you already typed is carried over).
How
- “Reset your password”: check the Email and tap Send code.
- “Enter the code”: the screen says “If an account exists for <your email>, we've sent a 6-digit code. It expires in 10 minutes.” It says this for every email, so the app never reveals who has an account. Type the 6-digit code from the email.
- Type a New password (same six-rule checklist as sign-up) and Confirm new password.
- Tap Reset password. You land on sign-in with “Password reset. Sign in with your new password.”
A wrong or old code shows “That code is wrong or has expired. Check the email address, or create an account if you don't have one yet.” Test mode only: on a local or test backend (every code is 111111, no email is sent) red text under the title says when no code was issued: no Nova account uses this email, the account is disabled, or its email is not verified yet. Production never shows it. Resend code works after the 30-second countdown. Wrong email? Back (or the back arrow) returns to the email step.
Devices list
What: every box your account owns, one card each. Where: the Devices tab, sub-tab Devices (the first thing you see after sign-in). Who: the owner; you only ever see your own boxes.
What a card shows
- The name (yours, or the model name if you never named it), the serial underneath, then the model and how many channels it has.
- “Connected to Nova” while it is online, otherwise “Last seen 5 min ago”, or “Not connected yet” for a box that has never come online.
- Badges: online (green); offline (amber, only once it has been away longer than a short grace period); fault (red, the box reports a problem); blocked (red, Nova blocked it); transfer pending (amber, you offered it to someone); factory test (grey, claimed by a test-station account).
Nearby: while the app is open it listens for boxes calling out over Bluetooth. A box of yours that is offline and close by shows as nearby (the blue Bluetooth mark) without the app connecting to it; the link opens only when you open that box.
Tap a card to open the device page. If you own more boxes than fit in one page, a line says “N of M shown”.
No devices yet: the screen says so and offers Add device.
The three sub-tabs
| Sub-tab | See |
|---|---|
| Devices | This list. |
| Moments | Moments across devices |
| Transfers | Transfers history |
Add a device (scan the label, type it, or find the box nearby)
What: “claiming” a box: binding it to your account. The label on the box carries a QR code, the serial (for example NVA-SB-00042) and a claim code printed as XXXXX-XXXXX. Having the label is the proof that you have the box. Who: any signed-in owner with a verified email. When: once per box, when it arrives, or after its previous owner released it.
Where: Devices › the + (Add device) icon top-right, or Add device on the empty list.
By QR (the default)
- The screen “Scan the label” opens with the camera. The first time, Android asks for camera access: allow it.
- Point the camera at the QR on the device label. There is no button to press: as soon as the code is read, the app claims the box.
- Go on with Name it below.
- Camera refused: the app says so and offers Allow camera, or use the typed path.
- Not a Nova label: “That QR code is not a Nova label. Try the printed serial and code instead.”
- You scanned a transfer QR here by mistake: the app switches to Redeem a code with the code filled in.
By typing the label
- On the scan screen, tap Enter serial & code instead.
- Type the Serial and the Claim code (10 characters; dashes and spaces are ignored, letters are made capital for you).
- Tap Add device. Scan the label instead takes you back to the camera.
An unknown serial or a wrong code is reported under the matching field.
Name it
- “Device added” shows the model and serial. If the box was already on your account, it says “Already yours” instead.
- Optionally type a name in Name this device (optional) (up to 32 characters, shown only to you).
- Tap Done (no name) or Save name; Skip drops the typed name. The device page opens.
Someone else already owns it
“Registered to another account” means the box is bound to a different account. The app never tells you whose. If you bought it second-hand, ask the seller to release it from their Nova app. If you are the rightful owner, contact Nova support with your proof of purchase; support verifies it and unbinds the box. Try another label returns to the typed form; Done leaves.
The box cannot be claimed right now
- In repair: “… is in for repair and cannot be claimed yet. Try again once it is back in service.”
- Not through its final factory check: “… has not finished its final factory check… Contact Nova support.”
- Taken out of service: “… was taken out of service…” — it can never be claimed.
The back arrow (or Android Back) leaves the flow from any step and returns to Devices.
By Bluetooth: Find the box nearby
For a box without its label at hand, or a box with no internet yet. Stand next to it.
- On the scan (or typed) screen, tap Find the box nearby.
- The first time, the app explains why it needs Bluetooth; tap Allow and accept Android's “Nearby devices” question (Android 11 and older ask for location instead; Nova never records where you are). If you refused before, the card offers Open settings. If Bluetooth is off, tap Turn on Bluetooth.
- The list shows every Nova box ready for setup in range, strongest signal first (usually the closest). Boxes that already have an owner never appear.
- Tap the box. The app connects, asks the box for a one-time claim token (valid 2 minutes, usable once) and adds the box to your account. Go on with Name it.
- Nothing found after a few seconds: “No unclaimed boxes found”. Move closer and tap Search again, or use the label.
- “The box hands out one claim token every 5 seconds”: wait the seconds shown and tap the box again.
- If the box is not connected to the internet yet, the name step says so and its button becomes Set up Wi-Fi: the app opens the new device's Settings tab, where the Wi-Fi controls appear once it connects over Bluetooth (see “Reach a box over Bluetooth”).
Claim and transfer links
What: the label QR and a transfer share link are ordinary web links: https://claim.novacube.io/c#… (a box label) and https://claim.novacube.io/t#… (a transfer). When: you scan the label with the phone’s camera app, or tap a share link someone sent you.
How
- With the Nova app installed, Android opens the app directly (the app is verified for these links). If you are signed out, sign in first; the app remembers the link.
- A label link opens Add device and claims the box at once; a transfer link opens Redeem a code with the code filled in and looked up. Back leads to Devices.
- Without the app, or if Android asks which app to use and you pick the browser, a Nova page opens: “Claim your Nova box” or “Accept a Nova transfer”. Tap Open in the Nova app, or Get the app to install it. The page also shows the serial (label) or the transfer code (share link) with a copy button, so you can type it in the app: Devices › Add device › Enter serial & code instead (with the claim code printed on the label) or Devices › Redeem a transfer code.
- The part after
#(the label secret, the transfer code) stays on your phone: the page never sends it anywhere. - “This label QR is incomplete” / “This transfer link is incomplete”: the link was cut off. Type the label’s serial and claim code in the app, or ask the sender for the code.
- A test (QA) build of the app answers the QA links (
claim.qa.iot.novacube.io) instead.
Device page and Details tab
What: everything about one box. Where: tap a card on Devices, tap a row in Activity that names a device, or tap a push about it.
The frame
- The title is the device name with an online (green) or offline (grey) badge beside it, visible on every tab.
- A red banner above the tabs appears when something is wrong: “This device is blocked by Nova…” or one line per active fault, for example “Fault: overcurrent on Kitchen (channel 2), since 10:41. The device protected itself; check the load and the wiring.” The fault banner updates live and disappears when the box clears the fault. A fault Nova raises itself says so, for example “Fault: config mismatch, since 17:38. Nova raised it: the box runs config v7 but its seed does not match the released v7. It clears when the box reports the released seed.”; “identity wrong pi” means the box’s identity does not match Nova’s record: contact support. When the box refuses a part it does not recognise (a board or mixer that is not on its signed list of parts, or the list itself is missing) the banner reads, once however many such faults there are: “A part of this device is not recognised — service needed.” It never names serials or MACs; contact Nova support.
- Five tabs: Details · Channels · Scenes · Activity · Settings. They scroll sideways on a narrow phone.
Details tab
- Name, badges (blocked, transfer pending, factory test) and the serial.
- Model: the model name, then the model code and config version in brackets.
- Yours since: when you claimed it. Last seen: when it last talked to Nova, or “not connected yet”.
Rename
- Tap Rename (pencil). Greyed out while the box is blocked.
- Type up to 32 characters. Leave it empty to go back to the model name.
- Save name (enabled once the name changed) or Cancel.
The name is stored at Nova and shown only to you. Partners using your API keys never see it.
Channels: control your device
What: a channel is one thing on the box you can control: a light, an LED strip, a fan, a socket, an input selector. Where: device page › Channels. When: every day, once the box is online.
Before the controls
- “Connecting to Nova…” shows while the live connection opens.
- When the box is offline, a note says the box keeps working locally and controls return when it reconnects, with when it was last seen. Every control stays visible but greyed out, so you never see a toggle that pretends to work.
The rows
Each channel is one row: icon, name, a state word and, when the channel can be switched, a power toggle on the right.
- The state word follows the kind: a light is On / Off, a fan is Running / Stopped, a socket is Live / Off.
- Flip the toggle to switch the channel. The app waits for the box’s answer and shows what the box really did. If Nova or the box refuses, red text under the row says why.
- The command goes over Bluetooth when the app has a Bluetooth link to this box, otherwise through Nova. Same rows, same answer either way.
- No answer: the app tries once more by itself (the box never does it twice). If there is still no answer, the row reads No answer — checking… until the box reports its state, which settles the row either way.
- A dial whose command failed snaps back to the box’s real value; it never keeps showing a value the box did not take.
- Who changed it: when someone other than you in this app changes a row while you have the page open, a small line under it says who: the box’s screen, an integration, Bluetooth at the box, the box itself, or Nova support. The line goes away when you change that row yourself or the page reloads the box’s full state; a page you just opened shows no such lines.
- Tap the row (or its arrow) to expand it. Only one row is open at a time; opening another closes the first.
Inside an expanded row: four small tabs
| Tab | What it holds | How to use it |
|---|---|---|
| Control (first) | The controls the channel supports: a round dial for Dimming (%), a round dial for White tone (K), a colour wheel with the colour code under it, a White bar (0–255) on colour strips that have a white LED, a Source grid (every source as a chip in two columns, written as words, e.g. “Camera front”), a 2 × 2 Position grid (↖ Top left · ↗ Top right · ↙ Bottom left · ↘ Bottom right) on a channel whose picture can move, such as a video inset, and a button for one-shot channels: Restart (switches off for a moment, then back on), Unlock or Switch on briefly (on for a moment, then back off), Toggle. | Drag around a dial or tap a point on it; the value is sent when you lift your finger. Tap or drag on the wheel to pick a colour. Tap a source chip to switch to it. Tap a corner to move the picture there; the source stays as it is. A one-shot button acts once and the row shows the result. A power-only channel says “Power only: use the toggle on the row.” |
| Channel | Read-only facts: the channel number, its kind, and each capability with its limits (for example “dimming from 0 to 100 %”). The factory name if you renamed it. | Channel numbers never change; partner software addresses the channel by this number. |
| Name | Your name for the channel (up to 32 characters) and an icon from a fixed set (light, lamp, strip, fan, socket, switch, source, tv, speaker, heater, pump, door, blind, camera). | Edit, then Save. Factory defaults puts back the original name and icon. Names and icons are only for you; they never reach partners. |
| Curl | The same command as a ready-made partner-API call, with your current values filled in. | Copy curl puts it on the clipboard. Replace the key placeholder with one of your API keys and run it from a terminal or your server, not from a browser. |
Save the current state as a scene
While the box reports its state, the Channels tab shows a floating Save as scene button. See Scenes on one device.
Device Activity tab
What: the part of your activity feed that is about this one box: claims refused, transfers, releases, blocks, resets, faults. Where: device page › Activity.
Rows look the same as on the main Activity tab (see Activity feed) but are read-only here: tapping does nothing and does not mark them read. A new box says “Nothing has happened to this device yet.”
Device Settings tab: transfer, alerts, remote control, Wi-Fi, resets, release
What: the operations that change who owns the box or wipe it, kept away from the everyday controls. Where: device page › Settings. Up to eight sections, top to bottom: Transfer, Notifications, Remote control (accounts with developer access), Wi-Fi, Kiosk (boxes with kiosk screens), Streaming key (boxes that stream), Reset and, set apart at the bottom with a red title, Release.
How it reads: every section is closed at first and shows its name with one line about where things stand, so you can read the whole tab without opening anything — for example Transfer “Not offered” or “Transfer pending · expires 23 Oct”, Notifications “Offline and fault alerts on”, Remote control “App and integrations”, Wi-Fi “Opens here next to the box” or “Connected over Bluetooth”, Kiosk “Locked · PIN set · relock after 5 min”, Streaming key “Key 981faaac… · signed by this box”, Reset “Network or factory reset” or “Needs the box online”, Release “Hand the box back”. Tap a section to open it; tap it again to close it. Only one is open at a time: opening another closes the first. The open one stays open while you look at other tabs of the same device.
Sections that open by themselves: Wi-Fi opens when the page connects to the box over Bluetooth, so the Wi-Fi controls are in front of you. A section whose action failed (for example a remote-control choice that did not save, or a reset or release the cloud refused) opens so you see the reason. If the Offline and fault alerts switch or Cancel transfer fails, the reason appears under the last section.
Transfer
- Transfer this device… opens a dialog “Transfer this device?”. Type your Password and tap Create code. The code screen opens (see Send devices to someone). There is no device picker here: it is always this one box. Greyed out while the box is blocked.
- While an offer is open, the section instead shows “Transfer pending”, when it expires, and Cancel transfer. Confirming “Cancel the transfer?” stops the code; the box stays yours.
Notifications
Offline and fault alerts switch: on means a push when this box goes offline or reports a fault. Turn it off to silence those pushes for this box only. Property and security alerts (claims, transfers, sign-ins) cannot be turned off. The rows still appear in Activity either way.
Remote control (accounts with developer access)
What: who may control this box from afar — you in the app, your integrations (the software holding your API keys), or both. Who: only you, and only while your account has developer access (it does not matter whether the Developer tab is switched on). How: pick one and tap Save remote control. It needs the internet (“Needs internet” under the button while the phone is offline).
| Choice | The app | Your integrations | Use it for |
|---|---|---|---|
| App and integrations (default) | controls | control | Every box starts here. |
| Integrations only | shows the box, does not control it | control | A box your own system runs, e.g. through bookings: nobody undoes a booking from the app by accident. |
| App only | controls | refused | A box outside your business, or after an API key leaked. |
- Integrations only: the Channels rows, the Scenes ▶ buttons, Go live / Stop streaming and the kiosk Lock / Unlock / Set PIN… stay visible but greyed out, with “Controlled by your integration. Change it in Settings › Remote control.” Names, icons, editing scenes, kiosk pages, transfer, resets and release stay yours. A moment skips such a box with “controlled by your integration”. If the cloud refuses anyway you see the same sentence with the code
app_control_off. - Next to the box, over Bluetooth, nothing is blocked — whatever you choose here. That is the emergency path (it is also how Wi-Fi is set). What you change at the box is reported when it is back online: an Activity row “Changed at the box over Bluetooth” (for example “2 channels, Wi-Fi joined Studio”), and your integrations are told.
- Every change adds an Activity row (“Remote control set to Integrations only …”) and tells your integrations. If you press the Developer tab’s panic button or developer access is turned off, the choice is kept but greyed with “Not in effect: …”, and the app controls the box until it applies again.
Wi-Fi
The box's Wi-Fi is set up only at the box, over Bluetooth. Next to the box the page connects over Bluetooth by itself once the box is listening, and the Wi-Fi controls show here; until then a line says it is looking (see Reach a box over Bluetooth). Greyed out while the box is blocked.
Reset
Both resets do the same as the button on the box and never change who owns it. They need the box online; nothing is queued. While it is offline the buttons are greyed out and the app says to use the button on the box.
| Button | Confirmation | What happens |
|---|---|---|
| Network reset… | A dialog explaining the effect; tap Reset network. | The box forgets its Wi-Fi and Bluetooth pairings and goes offline. You must be next to it to set it up again; it starts advertising for setup within about 2 minutes. Names, scenes and ownership are unchanged. A box that reaches Nova over Wi-Fi only cannot be controlled remotely until you set it up again. |
| Factory reset… (red) | A dialog listing what is wiped and what survives; press and hold “Hold to factory reset” for 3 seconds. A tap does nothing. | Wiped on the box: its settings, network and Wi-Fi, Bluetooth pairings and sessions on the box. Survives: your ownership, firmware and channel map, and your names and scenes. The box goes offline and must be claimed again at the box with its label (about 2 minutes). |
After either reset a message at the bottom confirms it was sent, and a row is added to the device’s Activity tab. If the box went offline in the meantime, the dialog stays open and shows the reason.
Release
Release device… (red) gives the box up. A dialog “Release this device?” asks for your Password; tap Release. You return to Devices with “Device released. Anyone with its label can claim it now.” Your name for it ends, and partner access through your API keys ends. This cannot be undone from the app; to get it back you claim it again with the label.
A wrong password is shown under the password field (reauth_failed).
When to use which: selling or giving the box to someone with a Nova account → Transfer (they get it directly). Throwing it away or handing it to a shop → Release. Moving it to a new Wi-Fi → Network reset. Starting over on the box itself → Factory reset.
Scenes on one device
What: a scene sets several channels of one box at once with one tap (“Evening”: lights at 30 %, strip warm white, fan off). Scenes are stored at Nova, not on the box. Where: device page › Scenes. A box with no channels cannot have scenes.
Apply a scene
- Each row shows the scene name and “k channels · scene n”. Tap the play button (▶) on the right.
- The box must be online. Offline, the play buttons are greyed out and a note says scenes can be edited now and applied when the box reconnects.
- If some channels refused, red text under the row names them (“Applied, except channel 3 (…)”). If the box refused the whole scene, the text says so.
Create a scene: two ways
- Snapshot: on the Channels tab, set everything the way you like, tap Save as scene, type a Scene name and tap Save. Every channel’s current state becomes the scene. A message “Saved as scene …” offers View, which opens it on the Scenes tab.
- Build: on the Scenes tab, tap Build a scene. A “New scene” card opens, filled with the box’s current values (or sensible defaults when offline). Edit it as below and tap Save scene, or Cancel.
Edit or delete a scene
- Tap the scene’s row (not the ▶). It expands into the editor; only one is open at a time.
- Name: up to 32 characters.
- “What this scene sets”: one line per channel with a tick box, icon, name, a summary of the value (“Running · 20 %”, “On · #FFAA00 · white 40”) and a power switch. Untick a channel to leave it as it is when the scene runs (“left as it is”). Tap a line to open that channel’s dials and wheel; they change the scene only, never the box.
- Save changes is enabled once something changed. A scene must set at least one channel.
- Delete (red) asks “Delete “name”?”. Integrations that apply that scene number get “scene not found” afterwards; the number is never reused.
Moments across devices
What: a moment fires one scene on each of several boxes at once (“Close up”: lobby box “Off”, studio box “Cleaning”). Where: Devices tab › Moments. When: once you own at least two boxes that each have scenes.
Apply
Tap ▶ on a row (“k devices · moment n”). If some boxes did not follow, red text names them, for example “Applied on 1 of 2 devices · Pod lights: offline”. Other reasons: “blocked by Nova”, “no answer”, “no longer available”, “partly (channel 3 refused)”.
Create, edit, delete
- Tap the floating New moment button, or tap an existing row to expand its editor.
- Type a Name (up to 32 characters).
- “What this moment fires”: for each of your boxes, open its drop-down and pick one of its scenes, or Not included to leave that box alone. A box without scenes shows “No scenes”.
- Save moment / Save changes (at least one scene must be picked). Cancel closes a new one.
- Delete asks for confirmation. The scenes it fired stay on their boxes; the number is never reused.
If a box left your account or a scene was deleted, the editor says that entry is no longer available; saving drops it.
Stream channels and keys
What: your live-streaming channels (Twitch, YouTube, Facebook, Kick or any RTMP server), the same list on every Nova box you own. Where: Account › Streaming › Stream channels. Who: the owner. Nova stores only each channel's name and server; the stream key stays on this phone and never reaches Nova.
Add a channel
- Tap Add a channel (or the + in the top bar).
- Type a Name (how it shows on your boxes, e.g. “My Twitch”), pick the Platform. Twitch, YouTube and Facebook fill in the Server URL; for Kick or another service paste the rtmp:// or rtmps:// address from its stream settings, without the key.
- Paste the Stream key from the platform and tap Add channel. The key field never suggests or corrects words (a keyboard suggestion would change the key), and spaces at either end are removed when it is saved.
- The phone asks you to confirm it is you (fingerprint, face or the screen lock). The key is then kept encrypted on this phone.
The phone needs a screen lock (PIN, pattern, password; a fingerprint or face on top is fine). Without one, Stream channels, the add/edit screen and the box's Stream tab first show a red card “Set a screen lock to keep stream keys on this phone.” with Set a screen lock, which opens Android's lock settings. Until it is set there is no key field and no Go live that needs this phone's key. Set the lock, come back, and the card goes away by itself.
Edit, show, delete
- Edit: change the name, platform or server; leave the key empty to keep the one on this phone. Show key asks you to confirm it is you first. Saving also asks.
- Delete asks for confirmation (no fingerprint needed). The channel disappears from all your boxes and their kiosks; its key is removed from this phone and forgotten on any box it was allowed on.
- Each card says key on this phone or no key on this phone. Keys are not copied between phones: on a new phone, open the channel and paste its key again (you can also reset it on the platform). A box that keeps the key (Allow on this box's kiosk) can still go live from the new phone without it — see Go live from a box.
Go live from a box
What: start and stop a live stream of the box's video. Where: device page › Stream (only boxes with a video mixer have this tab). When: the box must be online, and it streams to one channel at a time.
Go live and stop
- Under Your channels, tap Go live next to a channel. Confirm it is you. The key is locked so that only this box can open it, and sent through Nova to the box.
- The card at the top shows connecting, then LIVE with the channel name, the start time, a good connection / weak connection badge and the speed (Mb/s). If it is still connecting after 20 seconds it adds “Still connecting. If this lasts, check the stream key for this channel.” with Check the key, which opens the channel (a wrong key is the usual cause: the platform never accepts the stream).
- Stop streaming ends it. If a stream ends by itself, the card says why (“the connection to the platform was lost”, “the platform did not accept the connection (check the key)”) and you get a notification.
Which key is used
- Key on this phone (“· key on this phone” under the channel name): Go live asks you to confirm it is you and sends this phone's key, locked to the box. This is used whenever the phone has the key, even if the box keeps one too.
- Key only on this box (you switched on Allow on this box's kiosk earlier, for example from another phone): the row says “Uses the key stored on this box.” and Go live starts at once — no fingerprint, nothing is sent from this phone. If the box no longer has it (it was reset, changed owner, or its streaming key was reset) you see “This box no longer has the key for this channel. Add the key on this phone, or allow it on this box's kiosk again.”
- Key on neither: the row says “Your stream key isn't on this phone.” with Add key on this phone (opens the channel to paste it) instead of Go live.
Before sending a key, the app checks that the box's streaming key is signed by the box itself (with the identity in its ownership certificate, remembered on this phone); if that check fails nothing is sent and you see “This box's streaming key could not be verified; nothing was sent.” A test box without an identity shows “Streaming key not verified (test box).”
Allow on this box's kiosk
Switch it on for a channel to let anyone at this box's screen go live on it without your phone. You confirm it is you; the key is stored on this box only, locked to it. Switch it off to make the box forget it. Every key allowed on a box is wiped if the box changes owner, is factory reset, or its streaming key is reset.
Partner channels
When a partner integration offers channels on this box (for example for a booking), they are listed read-only with the time they expire. They are started from the box's kiosk or by the partner.
Reset streaming key
Device page › Settings › Streaming key shows the box's key id. Reset streaming key… asks for confirmation; the box makes a new key and wipes every key you allowed on its kiosk. Channels and the keys on your phone are unchanged.
no_stored_key), “The box changed its streaming key. Try again”, “Not confirmed — nothing was sent.”Kiosk: PIN lock and pages
What: control who can use the box's touch screens and what they show. Where: device page › Settings › Kiosk (only boxes with kiosk screens). Who: the owner; a partner integration can do the same through its API.
PIN lock
- Set PIN…: type 4–6 digits twice and tap Set PIN and lock. Both screens lock at once and show a PIN pad. The box keeps only a scrambled copy of the PIN.
- Anyone who types the right PIN on either screen unlocks both. Anyone at an unlocked screen can tap Lock (top right).
- Lock / Unlock here work from anywhere, without the PIN. Lock stays greyed until a PIN is set.
- Five wrong PINs freeze the pad for 5 minutes and you get a notification; Unlock in the app clears it.
- Locking only blocks the screens: a live stream, the lights and the camera keep running.
Pages shown
Tick the pages the screens may show: Instructions, Camera control, Ambience (lights, fans), Device setup, Streamers studio · your channels, Streamers studio · partner channels. Pick Lock again automatically (off, or after 5–60 minutes without a touch) and tap Save kiosk pages. A partner integration can hide more pages but never show one you hid; such a page says “Hidden by your partner integration”.
Using the app without internet
What: the app keeps a copy of your devices, scenes, moments, transfers, recent activity and stream channels on the phone, so it opens and shows them without internet. When: whenever the phone cannot reach Nova (no signal, airplane mode, Nova unreachable). When the connection is back, the app fetches only what changed, by itself.
- A line at the top of every page says Offline · last updated at 14:02 · actions that need the internet are paused.
- Buttons that need Nova (rename, transfer, release, resets, kiosk and streaming settings, editing scenes and moments, adding a device, redeeming a code, mark all read) stay where they are, greyed out, with Needs internet under them.
- A box's state shows as unknown (an empty grey ring instead of the dot): the phone cannot know whether the box is online while it is offline itself.
- Next to the box you can still control it: open its page and the app connects over Bluetooth by itself once the box is listening (after 2 minutes without Nova, or for the full 10 minutes after a short press of its button). The link then stays open while you move around the app. See Reach a box over Bluetooth.
- Opening an unread row in Activity marks it read on the phone; Nova is told when the connection is back.
Reach a box over Bluetooth: local control and Wi-Fi
What: talk to a box directly from your phone, without the internet: switch its channels and set up or repair its Wi-Fi. Who: the box's owner, on the phone they are signed in on. When: only while the box is listening over Bluetooth, which is when it has been offline for 2 minutes or more, or for 10 minutes after a short press of its button. The button's 10 minutes run in full: connecting, leaving and coming back inside them needs no second press. A healthy online box stays silent; use the normal controls then.
Where: the box's normal device page. There is no separate Bluetooth screen: when the box is offline and you are next to it, the device page connects over Bluetooth by itself and the Bluetooth mark beside its name turns blue. On the Devices list, each box shows the same two marks, always: a dot for Nova (green online, amber offline for a while, grey offline, an empty ring when your phone is offline) and the Bluetooth mark (blue when the app hears the box nearby and Bluetooth is already allowed, grey otherwise). If Bluetooth is off, one line under the list says so.
Connect
- Open the box. The channel rows stay on screen with their last known values, greyed while there is no link; the page never swaps them for a spinner. If the app needs Bluetooth permission, or Bluetooth is off, the Channels tab shows the card asking for it above the greyed rows (same steps as “Find the box nearby”); the rows stay.
- When the box is listening, the link opens by itself: the badge beside the name reads connecting nearby…, the app checks both ways that this is your box and your phone (nothing is sent to a box that cannot prove who it is), then the badge reads via Bluetooth and the rows go live with what the box reports.
- The link belongs to the app, not to the page. It stays open while you move to other pages, other boxes or turn the phone, and coming back to the box is instant. It closes 30 seconds after the app goes to the background, when you sign out, when the box leaves your account or is in repair with its new identity not yet active, when the box hangs up after 5 minutes without use, or when the box is back online and you are not on its page (Nova takes over).
- Up to 3 boxes can be linked at once, so you can walk between boxes at a venue without reconnecting. Opening a fourth closes the one you used least recently.
- If the box comes back online while you are on its page, the page stays on Bluetooth until you leave it.
Channels and scenes
The same rows as always, now showing what the box itself reports. Tap the power switch, or open a row for dimming, colour, the video mixer and the other controls; each change shows the box's answer. On the Scenes tab, ▶ sets the scene's channels directly on the box. Renaming, transfers, resets, kiosk and streaming settings need the internet.
Wi-Fi
Device page › Settings › Wi-Fi. While the page is connected over Bluetooth, the Wi-Fi controls appear right there. Nothing to tap: next to the box, the page connects by itself as soon as the box listens — at once while it is offline. An online box is not open for local changes: to edit its Wi-Fi details, short-press its button — it switches to local mode for 10 minutes (it stays connected to Nova) and the Wi-Fi settings open on the page by themselves.
- The card shows whether the box reaches Nova, its wired network, and whether Wi-Fi is set up.
- Tap Scan for networks: the box lists what its own radio can see (signal and security). Tap a network, type the password (8–63 characters) and tap Join. A network not in the list: Hidden network…, then its name and password.
- The result is spelled out: joined (the box then connects to Nova and Bluetooth goes quiet), wrong password, network not found, or took too long. Nothing changes on a failure.
- Clear Wi-Fi… makes the box forget every Wi-Fi network (confirm in the dialog). A wired connection keeps working.
The password goes only to the box, encrypted, and is never kept on the phone. Wi-Fi can be changed only this way, never from the cloud or a kiosk.
When it does not connect
- The page keeps saying the box is offline and never connects: the box is not listening, or you are too far away. Stay close; a box listens once it has been offline for 2 minutes, or for 10 minutes after a short press of its button.
- A red line with Try again on the Channels tab: the app could not open the connection for the reason given (the messages below); the page stops retrying by itself until you fix it.
- “This phone has no offline pass for this box yet” / “has expired”: connect the phone to the internet and open Devices once; the app refreshes the pass for every box. It is valid 14 days.
- You do not have to wait for this message: when this phone has not been online for about 11 days, so its passes run out within 3 days, it shows one notification, “Go online to keep offline access”, naming a box and the date. Connect to the internet and open the app (tapping the notification opens that box); the passes renew and the reminder goes away. It works without internet and is the only notification Nova makes on the phone itself.
- “This sign-in predates Bluetooth access”: sign out and sign in again on this phone.
- “A newer sign-in has already opened this box”: another phone signed in to your account later; refresh this phone online.
- “This box has no verified identity yet” or “could not prove it is the one in your certificate”: nothing was sent; contact Nova support.
- “This box is being repaired and its new identity is not active yet, so the app cannot open it over Bluetooth. It works again once the repair is finished.”: the box is in repair (for example its main board was replaced). After the repair the app fetches the box's new ownership certificate by itself; nothing to do.
- While connected, the app also refreshes the phone's pass through the box. If that fails, a red line says so; the connection keeps working.
Send devices to someone
What: handing one or more boxes to another Nova account with a single-use code. You keep full control until the receiver accepts. Who: the current owner. When: selling a box, or moving it to a colleague’s or company account.
One box
Device page › Settings › Transfer this device… › type your password › Create code.
Several boxes
- Devices › Transfers › Transfer devices.
- Tick the boxes to hand over (tap the row or its tick box). A count shows “N selected”. A box that already has an open offer is marked as already offered; the new code replaces the old one.
- Type Your password. It is asked every time property is handed over.
- Tap Create transfer code (or Create code for N devices).
The code screen (shown once)
- The code in large letters,
XXXXX-XXXXX. - Copy code and Copy link put the code or a share link on the clipboard (“Code copied”, “Link copied”).
- A QR of the link. The receiver can scan it from their app (Devices › Redeem a code).
- A summary: how many devices, when it expires (“expires in 3 days”), and that you keep control until it is accepted.
- Done takes you to Devices › Transfers.
Receive devices (redeem a code)
What: accepting boxes someone offered you. Where: Devices › the Redeem a transfer code icon (top-right, left of +). Who: any signed-in owner other than the sender.
How
- Type the Transfer code (10 characters; dashes optional), or tap Scan a transfer QR and point the camera at the sender’s QR (Hide scanner closes it). A scanned code is looked up straight away.
- Tap Look up.
- Check the preview: “N devices offered”, each with a partly hidden serial and the model, and Code valid until. The sender is not shown.
- Tap Accept device (or Accept N devices). Accepting makes them yours and ends the sender’s access. Names and scenes start fresh.
- You return to Devices with “N devices are now yours.”
A box that cannot change hands right now is refused, for sending and accepting alike: “… is in for repair and cannot be transferred yet. Try again once it is back in service.”, “… has not finished its final factory check… Contact Nova support.” or “… was taken out of service…”.
Problems with the code show under the field: not found, expired, already closed, or your own code (transfer_not_found, transfer_expired, transfer_closed, transfer_self). Ask the sender for a new code.
Transfers history and cancelling
What: every transfer you sent or accepted, newest first. Where: Devices › Transfers.
- Transfer devices at the top starts a new offer (see Send devices to someone).
- Each card: “Sent N devices” or “Received N devices”, a status badge (pending, accepted, cancelled, expired), the full serials and models, and dates (Created, Expires, Accepted, Cancelled with the reason, Expired).
- Cancel reasons: “you withdrew it”, “replaced by a newer offer”, “a device left your ownership”.
Cancel an offer
On a pending offer you sent, tap Cancel transfer and confirm “Cancel the transfer?”. The code stops working and the boxes stay yours. You can also cancel from a box’s Settings tab.
Empty: “No transfers yet” with a Transfer devices button.
Activity feed
What: the one place for everything Nova tells you: claims on your boxes, transfers, sign-ins elsewhere, support actions, faults, offline boxes, integration problems. Pushes are only a copy of these rows, so nothing is lost if a push never arrived. Where: the Activity tab. The number on its icon is your unread count.
Top to bottom
- Notifications are off (Android 13 and newer, until you allow them): tap Allow.
- Needs attention: things still open, which stay until they are resolved. A fault on a box, a box that is offline, a transfer still pending, and (developers) a paused webhook or retiring API keys. Tap a row to go to the box, the webhook or the keys.
- Filter chips: All · Security · Devices · Integrations.
- The feed, grouped under Today, Yesterday and dates. Unread rows have a dot, a bold title and a new badge. Property and security rows carry a red security badge; developer rows an integration badge. Each row has a short text and how long ago; “· open device” means tapping opens that box.
- Tap a row to mark it read (and open its box when it names one).
- The double-tick icon top-right (Mark all read) appears while anything is unread.
- New rows appear at the top by themselves while the app is open.
- The tab shows your latest 50 rows; below them a line says “Showing the latest 50 of N.”
Your own actions (a transfer you started, a reset you sent) are recorded here but are not pushed to you.
After a repair: “Device repaired” (“<name> was repaired; its identity was renewed”, also a push) when Nova service replaced the box's main board or renewed its identity — the box stays yours with its names, scenes and integrations; and “Device tested” (in the feed only, no push) when service tested the box after the repair. The app refreshes the box's ownership certificate after a repair by itself.
Push notifications
What: phone notifications for new Activity rows, even when the app is closed. When: set up once, right after your first sign-in.
How to turn them on
- Open the Activity tab. On Android 13 and newer a card says “Notifications are off”.
- Tap Allow and accept Android’s prompt. On older Android versions they are on already.
Android lists four Nova notification categories you can tune in the phone’s settings:
| Category | Covers | Mute in the app? |
|---|---|---|
| Property & security | Claims on your devices, transfers, sign-outs, support actions. | No. |
| Availability & faults | A device went offline or raised a fault. | Per device: Settings › Offline and fault alerts. |
| Integration health | Webhook paused, API key about to expire. | No. |
| Offline access | A reminder made on the phone itself (no internet needed): this phone has been offline for about 11 days and its Bluetooth passes to your boxes run out within 3 days. At most one per renewal; it disappears when the phone renews them online. | Only in Android settings. |
Tapping a notification opens the box it is about, or the Activity tab. Signing out stops pushes to this phone. A force-stopped app receives nothing (Android rule).
Account tab
What: who is signed in and your account settings. Where: the Account tab.
- Profile card: initial, display name, email; a red “email not verified” badge if that applies.
- Security: Change password (next section) and This device (the phone this sign-in belongs to).
- Developer: your developer access row (see Developer access).
- Help: Tips and tours (replay any tip), Show tips again and Open the guide (this book). See Tips and tours.
- About: the app version and the Nova server it talks to. Quote both when you contact support.
- Sign out (red, at the bottom).
- The circular-arrow icon top-right (Refresh account) reloads your profile and developer access from Nova.
Changing your display name or email is not in the app yet.
Tips and tours
What: short tips that point at the real buttons on a page: a soft light falls on one control and a small card explains it. When: the first time you sign in (three tips on Devices), the first time you open a page or a feature appears (a page’s own tips), and once after an update for a new feature (“what’s new”). Who: every owner; the Remote control tip only for accounts with developer access.
How they behave
- Each card says where you are (2 of 4) and has Skip and Next (Done on the last one).
- The page keeps working underneath. Tapping the lit control does what it always does and moves the tips on; tapping anywhere else, Skip or the Back button closes them.
- At most one set of tips starts by itself each time you open a page, and one “what’s new” each time you open the app. Tips never start over a dialog, while a page is loading, or when you opened the page from a notification or the “nearby” card.
- Tips never press anything for you, never ask for a permission and need no internet.
- Once you finish or skip a set, it does not come back — on this phone, on your other phones, or after reinstalling: Nova remembers it for your account. Skipped while offline? The phone tells Nova when the connection is back.
- With TalkBack on, each tip is read as a small card (title, “Step 2 of 4”, text) with Skip and Next; Back closes it.
- An account created after a feature shipped meets it through the page’s own tips instead of a “what’s new”.
Every tip, word for word
| Tips | When | Steps |
|---|---|---|
| Welcome | First sign-in, on Devices |
|
| The device page | First device page |
|
| A channel’s controls | First channel row opened |
|
| Scenes | First Scenes tab with a scene |
|
| Moments | First Moments tab |
|
| Activity | First Activity visit |
|
| Reached over Bluetooth | First time a page reaches the box over Bluetooth (what’s new 2026-10-21) |
|
| Wi-Fi over Bluetooth | First visit to Settings › Wi-Fi |
|
| Offline | First time the offline line shows (what’s new 2026-10-21) |
|
| Remote control | What’s new 2026-10-21, developer access only, device › Settings |
|
| Stream keys | First Stream tab or Account › Stream channels |
|
Help: replay, start over, read the guide
Where: Account › Help.
- Tips and tours: every set of tips your account can see, with whether you finished or skipped it. The ▶ button opens its page and plays it again. Replaying changes nothing: a tip you finished stays finished. A tip for something that is not on the page right now (offline, Bluetooth) shows as a card at the bottom.
- Show tips again: after a confirm, every tip starts again the next time you meet it, on all your phones. Needs internet.
- Open the guide: opens this book in the browser.
Change password
Where: Account › Change password. When: any time you know your current password (otherwise use Forgot password).
- Type your Current password.
- Type a New password (same six-rule checklist) and Confirm new password.
- Tap Change password. You return to Account with “Password changed”.
A wrong current password shows under that field; a new password that breaks a rule or equals the old one shows under the new one. This phone stays signed in.
Developer access
What: permission to connect partner software to your boxes with API keys and a webhook. It is off by default and Nova reviews each request. Who: owners who build or run an integration; most owners never need it. Where: Account › Developer.
| The row says | What it means | What you can do |
|---|---|---|
| Developer access | Not requested. | Request access: a dialog asks “What will you build?” (optional, up to 300 characters); tap Send request. |
| Developer access requested | Waiting for Nova’s decision since the date shown. | Wait. You are told in Activity and by email. |
| Developer settings (switch) | Granted. | Switch on to add the Developer tab to the bottom bar. Switching off hides the tab; your keys keep working. |
| Developer access not granted | Nova declined, with its reason. | Request access again once the date shown has passed. |
| Developer access suspended by Nova | Nova revoked it, with its reason. | Contact Nova support to restore it. |
Developer tab: API keys, webhook, emergency stop
Where: the Developer tab, present only while access is granted and the switch is on. If Nova revokes access while you are on it, the tab disappears and the screen explains why. The API and the webhook themselves are described in the Partner API book.
Overview
- “Needs attention:” badges for retiring keys, a failing webhook or a paused webhook.
- Rows API keys (“N active · N retiring”) and Webhook (Not configured / Healthy / Failing / Paused).
- Emergency: Suspend all partner access. Press and hold (about 1.5 seconds; a tap does nothing). Every key and webhook delivery stops at once. A red card “All partner access is suspended” then offers Resume, which asks for confirmation.
API keys
- Tap Create key. Give it a Label and pick Scopes: COMMAND (send commands and scenes) and/or READ (read state). Tap Create.
- The key is shown once. Use Copy (marked sensitive on the clipboard) or Show QR, store it in your server’s secret store, then tap Done. Lost it? Rotate the key.
- Each key card: status (active, retiring with its countdown, revoked), scopes, created, last used and from where, “Last 24 h: N requests”, and which key it replaces.
- Usage opens the key’s request log (method, path, when, from where, result). Load more pages back.
- Rotate (active keys) issues a new key at once and shows it once; the old one keeps working for a while as retiring so you can switch over.
- Revoke / Revoke now asks for confirmation; the key stops working immediately for everyone who holds it.
Webhook
- Register: type the Endpoint URL (HTTPS), optionally pick event types (none = all), tap Register. The signing secret is shown once, like a key.
- Edit URL › Save URL: the new address starts healthy; the secret stays.
- Events › Save filter changes which events are sent.
- Send test event shows the result inline (“delivered, 200 in N ms” or the failure).
- Re-enable appears when Nova paused a failing webhook. Missed events are not replayed; read the current state through the API afterwards.
- Rotate signing secret: a new secret is shown once; the old one keeps working for 7 days.
- Remove webhook: deliveries stop and the secret is discarded.
- Recent deliveries: each delivery with its result; Load more for older ones.
Troubleshooting
| You see | Why | What to do |
|---|---|---|
| Back on the sign-in screen with a red banner | You signed in on another phone, the session expired, or support signed you out. | Sign in again. Only one phone can be signed in at a time. |
network_error | The phone cannot reach Nova. | Check the phone’s internet and try again (most screens have a Retry button). |
| “Connecting to Nova…” does not go away | The live connection cannot open. | Check the internet; leave the app and come back. |
| Controls greyed out, box offline | The box has not talked to Nova recently. It keeps working locally. | Check its power and network. Controls return when it reconnects. |
| “Registered to another account” when adding | Someone else owns the box. | Ask the seller to release it, or contact support with proof of purchase. |
| Camera does not open when scanning | Camera permission refused. | Tap Allow camera, or allow it in Android settings, or type the serial and code. |
| “That QR code is not a Nova label” | You scanned another QR. | Scan the QR on the device label, or type the serial and code. |
| Red fault banner on a device | A channel protected itself (overcurrent, temperature, short) or the box reported a problem. | Check the load and wiring on that channel. The banner clears when the box clears the fault. |
| “This device is blocked by Nova” | Nova blocked the unit. | Contact Nova support; it cannot be controlled or claimed meanwhile. |
| Transfer code “not found” or “expired” | Wrong code, or it ran out or was cancelled. | Ask the sender for a new code. |
| Remote reset buttons greyed out | The box is offline. | Use the button on the box. |
| Controls greyed out with “Controlled by your integration” | The box is set to Integrations only (Settings › Remote control). | Change it in Settings › Remote control, or control it next to the box over Bluetooth. |
| No push notifications | Notifications not allowed, the category muted, Offline and fault alerts off, or the app force-stopped. | Activity › Allow; check Android’s Nova notification settings and the device’s Settings tab. Everything is still in Activity. |
| No Developer tab | Access not granted, or the switch is off. | Account › Developer. |
When you contact Nova support, give the error code shown in grey, the app version from Account › About, and the box’s serial.
What the station is
The flashing station is the machine, and the person at it, that puts a released firmware onto a board. That machine needs no source code, no git and no PlatformIO. It runs one small program, the station agent (nova-station). The agent always asks this console which release is current for the board's revision, then downloads it, checks it, writes it to the board and confirms that the board runs it.
| Who | Uses | For |
|---|---|---|
| Operator (factory, bench) | nova-station flash | Flash boards, read PASS/FAIL |
| Operator (EOL rig) | nova-station eol board | Test one flashed board on the rig over the bus, port by port |
| Operator (imaging station) | nova-station image | Write a gateway Pi's card or disk with the sealed image, and give the box its identity |
| Operator (imaging station) | nova-station eol pi | Test one imaged master board (Pi): the box checks itself over its board LAN |
| Engineer | tools/release.py | Build one firmware version and its record |
| Engineer / admin | tools/catalogue_sync.py or the revision page | Put the board records and the release into this console, publish it, make it current |
Where it sits in the flow
- Hardware tests pass on a new board revision; its records exist in the repo.
- The engineer builds a release (Building and publishing a release).
- The release is uploaded here, published and made current (Console application › Board revision page).
- Every board of that revision is flashed at a station (Flashing a board).
- Each flashed board is tested on the EOL rig (Board EOL on the rig).
- A gateway Pi gets its card or disk written with the sealed image at a station (Writing a gateway Pi).
- Each imaged Pi gets its end-of-line test on the same cable (Master board (Pi) EOL).
What exists today and what is planned
- Today: releases in this console (upload, publish, make current, download), the agent's command line, the release script and the catalogue sync script.
- Today, too: the Stations page in the console, where admins issue and revoke station keys and see who is signed in and what was flashed; operators sign in on the agent with
nova-station login. - Today, as well: the Flash… button on the ERP board page and the catalogue revision page, which drives the agent on your computer: it checks that the person signed in on the agent is you (signing someone else out first, or signing you in with one click), shows an install panel when no agent answers, and the Stations page's Agents tab, where admins upload the builds per operating system, and Set up this computer…, where everyone downloads them (macOS: the .pkg; Windows: the setup program).
nova-station autostart onkeeps the agent's local service running at every login. - New in agent 0.8.0 (2026-10-26, the device map): the identity job (
cert pi,image --unit, the console's Install identity…) is now one sequence — a pre-check (the unit's signed device map from the console: its gateway must be the Pi on the cable, else a plain refusal naming the console's gateway swap and nothing is issued) →identity.csr→ issue →identity.install→ activate → fetch the map →map.install→identity.show+map.show. Newnova-station map install --unit … --rma …(a board-only RMA swap; the console's Install device map…) andmap show.eol --unitsays a refused start (eol_blocked) in words. The manualcert issueis gone: an identity is only ever installed with its activation and its map. See Writing a gateway Pi. - New in agent 0.6.3 (object storage, Pending row 30; the release that follows 0.6.2): every release the agent fetches — board firmware for
flashand Pi images forimage— comes through a 10-minute link the console issues for that one file in Nova's private bucket (POST /station/firmware/releases/<id>/link, same scopes as before:firmware.read, a Pi imagepi.image); the agent fetches the link without its station key or sign-in and checks the SHA-256 and size before caching. The console's old streamed download is gone, so agents 0.6.2 and older can no longer download a release (files already in the cache still work). If the firmware release is numbered differently, it is the first agent after 0.6.2 that fetches by link. - New in agent 0.6.2 (2026-10-24): the seed is checked by its fingerprint: the cloud's signature covers
seed_sha256(the SHA-256 of the seed), not the seed itself, soseed,image --unitandimage --seedcheck that it matches the seed sent beside it (seed_hash_mismatchotherwise, nothing written). An envelope of the old form, with the seed inside the signed payload, is refused. Nothing about downloads changed in 0.6.2. - New in agent 0.5.4 (2026-10-23): board EOL prompts gain Redo (runs that output's commands again so you can watch the load, then asks again; every attempt is kept in the result file), and the identity check compares the board's real Ethernet MAC rule (agent 0.5.3 and older failed every real board on chip (MAC)).
- New in agent 0.5.3 (2026-10-23):
nova-station eol pi, the master board (Pi) EOL, and the console's Run Pi EOL… on an MB board's ERP page. - New in agent 0.5.1 (2026-10-22):
nova-station seedwrites the seed and its cloud signature (product_config.sig), andimage --unitstages both;image --serialrefuses a sticker of another MB model before writing and, after the boot, writes the board's identity file and marks it SERIALISED;flash --efuse burnmarks a burned board SERIALISED;eol boardalso runs on SERIALISED boards. - New in agent 0.4.0 (2026-10-20):
nova-station eol board(board EOL on the rig; its result was not recorded centrally then — it is now, on the board row) and Pi imaging,nova-station image,targetsandcert pi(built, not yet proven on the bench; macOS and Linux only, run from a checkout of the firmware repo for now).
nova-station flash) or the console's Flash button. Neither needs the other.Flavours: dev, QA and production
One codebase, three programs (ruled 2026-10-26). Each Nova cloud has its own console and its own build of the agent, and the three can be installed side by side on one machine: each has its own name, settings folder and local port. The flavour is fixed when the program is built (station/installers/build.sh --flavour dev|qa|prod; the pipx install from a checkout has all three entry points), not a setting: a command that a flavour does not have is not in its --help at all. The agent line restarted at 1.0.0 for all three; the flavour is in the program name, never in the version.
| Flavour | Program | Settings folder | Local port | Console |
|---|---|---|---|---|
| dev | nova-station-dev | ~/.nova/station-dev/ | 8765 | set by hand: config set console_url / console_origin |
| QA | nova-station-qa | ~/.nova/station-qa/ | 8766 | built in: https://api.qa.iot.novacube.io, origin https://qa.iot.novacube.io |
| production | nova-station | ~/.nova/station/ | 8767 | built in: https://api.iot.novacube.io, origin https://iot.novacube.io |
What each one exposes
- nova-station-dev — everything in this book: the console-driven jobs and every terminal command, with the engineering options (
--image-file,--provision-src, the rawdooroperations,cloud locate,efuse off|virtual|burn,eol … --offline, the bench flags) andconfig setof every setting.NOVA_STATION_HOMEstill moves its folder (dev only). - nova-station-qa —
login,logout,whoami,doctor,serve,autostart,targets,image(console releases only),flash,eol(--unit,board,pi),cert pi,map install/map show,outboxandconfig(config settakes onlystation_keyand thestationname). No engineering options, no bench flags. - nova-station (production) —
login,whoami,doctor,serve,autostartandconfig(station_keyand thestationname). Every job starts from the console's dialogs (the local service). There is nologout: sign out with Sign out in the console's station panel.
What QA and production also enforce
- They refuse the environment overrides
NOVA_CONSOLE_URL,NOVA_STATION_KEYandNOVA_STATION_HOME, and refuse a console that answers as the bench cloud (bench: true). - eFuse is always burned at the serial step; it cannot be switched off.
Names per flavour
/healthcarriesflavour,programandport.- Autostart registers
io.novacube.nova-station-dev,io.novacube.nova-station-qaorio.novacube.nova-station(macOS); on Windows the logon taskNovaStationDev,NovaStationQAorNovaStation. - Installer names:
<program>-<version>-macos-arm64.pkg(package identifierio.novacube.nova-station-dev,-qaorio.novacube.nova-station, so the three coexist) and<program>-<version>-windows-x86_64-setup.exe. images.csvgains aversioncolumn.
.pkg on macOS, the setup program on Windows, install-helper.sh on Linux and for a pipx install from a checkout); until then targets says so and imaging stops before writing.How the console and the agent find each other
- Each console drives its own flavour only, on that flavour's port: the dev console looks on 8765, QA on 8766, production on 8767. Another flavour answering is named (“This is nova-station-qa, not nova-station-dev”) with the right installer.
- The agent sends its flavour and version on every call to the console (
X-Nova-Agent-Flavour); the console refuses a sign-in from another flavour (agent_flavour_mismatch) and shows flavour and version per station (Stations list and station page). - Each console's Set up this computer… offers its own flavour's installer only (
<program>-<version>-macos-arm64.pkg,<program>-<version>-windows-x86_64-setup.exe); developers may instead install the dev flavour with pipx from the checkout. doctorprints the flavour, the console it talks to (pinned or set), the settings folder and the port.
nova-station …. On a dev station type nova-station-dev …, on a QA station nova-station-qa …; a command the flavour does not have is not there.Install and first run
Once per machine. The agent is one program, and a station on macOS or Windows does every duty: flash, board EOL, Pi imaging, Pi EOL and the identity step. Install the flavour of the console you work with (see Flavours: dev, QA and production). Installers are built for two kinds of computer: macOS Apple silicon (an installer package, <program>-<version>-macos-arm64.pkg) and Windows x64 (a setup program, <program>-<version>-windows-x86_64-setup.exe), where the program is nova-station-dev, nova-station-qa or nova-station. Linux, macOS Intel and Windows ARM are not available yet: the console lists them that way and offers no download (engineers on Linux can still pipx install from a checkout).
| Need | How |
|---|---|
| The agent | No Python needed: download it for your computer from Console › Stations › Set up this computer… (the Flash dialog offers the same when no agent answers). macOS: open the installer package and enter the administrator password once (below). Windows: run the setup program once and accept the administrator prompt (below). Both install the program on the PATH and the writer helper. Developers can instead pipx install the agent folder from a checkout of the firmware repo (and run install-helper.sh, see First-time setup). |
| USB-serial driver (power board) | Built into macOS 13+ and Linux. Windows needs WCH's CH343 driver: the Windows setup program installs it when the build bundles it, otherwise install WCH's CH343SER yourself. |
| A USB data cable | Charge-only cables are the first suspect when the board's port never appears. |
| Network access to the IoT core backend | Plus this machine's station key: an admin creates the station in Console › Stations and copies the key, which is shown once. |
| 24 V bench supply | Only if loads are checked at the station. Flashing itself runs on USB. |
macOS: the installer package
- Download
<program>-<version>-macos-arm64.pkg(nova-station-dev,nova-station-qaornova-station) from Stations › Set up this computer… and open it. It is not signed yet: the first time macOS refuses to open it, go to System Settings › Privacy & Security, choose Open Anyway and open it again. - Click through the installer and enter the administrator password once. It installs the program to
/usr/local/bin/<program>(on the PATH) and the writer helper for Pi imaging: the root-owned/usr/local/libexec/nova-station-writeand/etc/sudoers.d/nova-stationwith its one rule for you — exactly whatinstall-helper.shdoes (the package runs the same script). Before it installs, it stops a running service of the same flavour. No terminal step, no checkout; the three flavours install side by side. - Then, in a terminal, the first-run commands below: the station key (dev: also
console_urlandconsole_origin),<program> login,<program> autostart on. - Check:
<program> doctor, and<program> targetswhose first line iswriter helper: ready. - Uninstall:
<program> autostart off, thensudo rm /usr/local/bin/<program>andsudo pkgutil --forget io.novacube.<program>; the helper goes withsudo sh install-helper.sh --uninstallfrom a checkout, orsudo rm /usr/local/libexec/nova-station-write /etc/sudoers.d/nova-stationonce no flavour is left. Your settings folder (~/.nova/station…/) stays.
Windows: the setup program
- Download
nova-station-<version>-windows-x86_64-setup.exeand run it; accept the administrator prompt (UAC). It is not signed yet: if SmartScreen stops it, choose More info › Run anyway. - It installs the agent to its flavour's own folder —
C:\Program Files\Nova Station Dev\nova-station-dev.exe,C:\Program Files\Nova Station QA\nova-station-qa.exeorC:\Program Files\Nova Station\nova-station.exe(production), so the three can be installed side by side — on the PATH, so the command is just the program name, the writer helper serviceNovaStationWriterfor Pi imaging (runs as the system; only the Windows user who ran the setup may talk to it), the firewall ruleNova Station board EOL (TCP 9000)(inbound TCP 9000 from192.168.137.0/24, the rig's Internet Connection Sharing network, fornova-station.exeonly) and the CH343 driver when bundled. - On its Console page enter the backend URL, this console's web address and (optionally) the station key: the setup then runs
nova-station config set …andnova-station autostart onas you. Left empty, run the first-run commands below in a normal PowerShell (not as administrator). - Check:
nova-station doctor, andnova-station targetswhose first line iswriter helper: ready. No sudo and no scripts on Windows. Uninstalling (Settings › Apps) removes all of it: the service, the firewall rule, the logon task and the program.
Getting updates
- Nothing updates itself.
nova-station doctorand the console's Flash dialog say when a newer build is published for your machine. Check your version withnova-station --version. - macOS: download the newer
.pkgfrom Stations › Set up this computer… and open it over the old one (administrator password once; it stops the running service first, and the helper is refreshed with it). Then<program> autostart onstarts the new build. Windows: run the newer setup program over the old one. - Installed with pipx (developers): get the latest firmware repo (
git pull), then runpipx reinstall nova-station.pipx upgradedoes not work for this kind of install. - Your settings, station key, sign-in and downloaded releases are kept across updates. Run
nova-station doctorafterwards.
First run, in a terminal
After the .pkg (macOS) or the setup program (Windows, when its Console page was left empty). A QA or production station only sets its key, signs in and switches the service on — the console is built in:
nova-station-qa config set station_key nst_… # (production: nova-station …)
nova-station-qa login # or sign in from any station dialog of the console
nova-station-qa autostart on
nova-station-qa doctor # flavour, built-in console, ~/.nova/station-qa/, port 8766A dev station (nova-station-dev; written nova-station below) also points the agent at its backend and console:
nova-station config set console_url http://<backend>:8080 # the IoT core backend, not the console's web address
nova-station config set station_key nst_… # from Console › Stations › New station
nova-station doctor # checks everything; fix what it flags
nova-station config set console_origin https://<console> # the console's web address, for its Flash button
nova-station login # sign in (below); the Flash button can also sign you in itself
nova-station autostart on # the OS runs the local service at every login from now ondoctor reports the agent version, whether esptool and the serial library are available, who is signed in and until when, whether the backend answers, and which serial ports exist.
Two credentials: the machine and the person
- The station key says which machine this is. An admin issues it on the Stations page and can revoke it. Revoking cuts the machine off at once.
- The operator session says who is working. Nothing is flashed without it, and every attempt is recorded in the station's Activity under that person.
What you may do: scopes
Each kind of station work is a named permission, a scope. Today: firmware.read (see revisions and download released firmware), board.flash (flash boards and record the result), inventory.write (board rows), inventory.test (board EOL), device.test (device EOL), device.cert.issue (a box's identity) and pi.image (Pi imaging from a console release; not in the console yet). More will come as the agent learns new jobs.
- An admin grants scopes to you on your user page (Permissions).
- An admin sets what is allowed on each station on the station page (Allowed on this station).
- You may do only what both allow. A change applies at once, even to a signed-in session.
- Without any allowed scope the console refuses to approve your sign-in. A job outside your scopes stops with "not allowed", naming the missing scope and where to ask.
nova-station login,whoamianddoctorprint what you may do.
Sign in and sign out
- Run
nova-station login. It prints a link and a code like68H3-MZJW. - Open the link in a browser where you are signed in to the console. Check that the page shows the same code, then press Approve. You become the operator of this station.
- The terminal says "Signed in as …".
nova-station whoamishows it again later. - The session lasts 12 hours, and ends after 30 minutes without station activity (a job, a sign-in, a report to the console). A running job counts as activity: from agent 0.6.6 the agent keeps the session alive while a job runs, so a long imaging or EOL run is not cut off halfway.
- The backend decides when a session ends, not the agent. From agent 0.6.6 every
/healthasks it, so the console's station panel shows the real state: "until 18:40 · signs out at 14:35 if the station stays idle (30 min without activity)" while it is live, and "Your sign-in on … ended — no station activity for 30 min / the 12-hour limit / signed out (by you or an admin)" once it is not, with the one-click sign-in (the agent has already cleared its copy). When the agent cannot reach the backend the panel keeps the last known answer and says it could not confirm it. Agents 0.6.5 and earlier report only their own copy of the 12-hour end. - When you leave the station, sign out:
logouton a dev or QA station (nova-station-dev logout,nova-station-qa logout); the productionnova-stationhas nologoutcommand — sign out with Sign out in the console's station panel. An admin can also sign you out from the station's Sessions tab. A disabled staff account is signed out everywhere.
nova-station logout every time you leave the station, even briefly, and lock the computer. The 30-minute idle sign-out and an admin's Sign out are a safety net, not a substitute.First-time setup of a station machine
Once per computer, after installing the agent. A station that images Pis needs one privileged piece the agent cannot install for itself, the writer helper; a station that only flashes ESP32 boards can skip it. On macOS the installer package already installed it, and on Windows the setup program did (below: how to check); on Linux, and for a developer install with pipx from a checkout, you install it once by hand. The console shows the same steps for your system in Stations › Set up this computer….
.pkg's postinstall runs install-helper.sh itself, so the steps below are only for Linux and a pipx install from a checkout; on a .pkg install go straight to "Check it worked". targets saying NOT READY on a .pkg install: open the package again.Also, always plug the card reader, USB enclosure or stick into a port directly on the computer, not through a USB hub: at the bench (2026-10-23) a stick behind a hub dropped off after a 22 GB write and the job stopped with read back stopped … No such file or directory.
macOS and Linux
On a Mac installed from the .pkg, step 1 is already done (skip to the check). Developers who installed with pipx, and Linux stations, run it by hand:
- Install the writer helper. Why: writing a card needs root, and the agent never runs as root. Instead one short, auditable root-owned script, the writer helper
/usr/local/libexec/nova-station-write(sourcestation/agent/helper/nova-station-write), may run without a password, and it does one thing only: write an image from your~/.nova/station/cache/to a removable whole disk and read it back. It re-checks everything itself (a whole disk, external and removable, not the disk this computer runs from, not virtual, at most 2 TB; the image a regular file owned by you inside the cache, no symlink, not larger than the disk) and refuses anything else. In a terminal on the station, from a checkout of the firmware repo:
sudo sh ~/Arduino/nova_boards_firmware/station/installers/install-helper.shIt installs the helper root-owned 0755 and writes /etc/sudoers.d/nova-station with exactly <you> ALL=(root) NOPASSWD: /usr/local/libexec/nova-station-write (checked with visudo -cf before it is moved into place). Running it again is harmless; --uninstall removes both. It replaces the old passwordless /bin/dd rule of agent 0.5.8 and older: that rule was effectively full root, since dd as root can overwrite any file, sudoers included.
Check it worked:
sudo -k; sudo -n -l | grep nova-station-write # → (root) NOPASSWD: /usr/local/libexec/nova-station-write
nova-station targets # first line: writer helper: readyThe Image a Pi… dialog no longer warns (the agent reports helper_ready: true). The console's job needs the rule, because the background service has no terminal to type a password into. Imaging from a terminal (nova-station image) works with the helper alone: without the rule, sudo asks for the administrator password once before the write, and the write and read-back are one sudo call, so the password cannot expire halfway. Without the helper both stop before writing and name this command.
- Bench / dev stations only: the bench cloud name (agent 0.8.3, ruled 2026-10-26). When this machine runs the local backend and console, add one line to
/etc/hosts:
sudo sh -c 'echo "127.0.0.1 dev-cloud.novacube.test" >> /etc/hosts'A bench backend serves boxes under the fixed name dev-cloud.novacube.test (.test is never public): the images' cloud address and the image and firmware download links (http://dev-cloud.novacube.test:9100/…), so the agent's downloads need the name to resolve to this machine. Check: nova-station doctor → bench name: dev-cloud.novacube.test → 127.0.0.1 (this machine); without the line it prints !! bench name (to do): … with the line to add. QA and production stations skip this step.
Windows
Nothing to install by hand: the setup program, run once as administrator, installed the writer helper as the Windows service NovaStationWriter. It runs as the system and listens only on a local named pipe that admits the Windows user who ran the setup (and the system), and it re-checks everything itself, like the macOS helper: a whole disk on USB or SD, not the disk Windows runs from, at most 2 TB; the image a regular file in your %USERPROFILE%\.nova\station\cache, no links, not larger than the disk. It takes every volume of the disk offline (locked and dismounted) for the whole write and read-back, so Windows cannot touch it in between. No sudo, no password prompt: the console's Image a Pi… and a terminal nova-station image both work as soon as it runs.
nova-station targets # first line: writer helper: ready (else NOT READY — and why)- NOT READY names the reason: the service is not running, or it was installed for another Windows user. Run the setup again as this user.
- Disks are named
\\.\PhysicalDrive2; you type the name (PhysicalDrive2) back to confirm, asdisk4on a Mac. - After a write Windows may say You need to format the disk in drive X: before you can use it about the Pi's Linux partition. Always choose Cancel; Format would erase the card.
- Plug the card reader or stick directly into the computer, not a hub.
- Bench / dev stations only (agent 0.8.3): as administrator, add
127.0.0.1 dev-cloud.novacube.testtoC:\Windows\System32\drivers\etc\hosts(the macOS step above says why;nova-station doctorchecks it).
Flashing a board
Before you start
Someone must be signed in on the station (nova-station whoami). The release must be published and current on the revision page. The agent never flashes an old or withdrawn version.
Steps
- Connect the board with a USB data cable. Run
nova-station portsif you are unsure which port it is. - Run
nova-station flash --revision PB/M1/RevE. Use the revision code exactly as the console shows it. - Follow the prompts. Type the answer shown in brackets, for example
continue. - Read the result. PASS means the board reported exactly the release's version, and its own model and revision. To ask a board again later without flashing, run
nova-station check --revision PB/M1/RevE. - When asked, scan the board's sticker (the scanner types the serial, for example
NVA-PB-00007-4, and presses Enter). The sticker was printed from the lot when the boards were received; the agent checks the serial is a board of the revision you are flashing.--serialgives it up front. - The serial step (
--efuse burn). What: after a passed flash, the agent burns the serial into the chip's eFuse, reads it back and marks the board SERIALISED in the Inventory; a chip that already carries its serial on a row still at FLASHED is recorded at that point; a refusal fails the job with the reason. Where: a production flashing station. Who: an operator withboard.flash+inventory.write. How:nova-station config set efuse burn(or--efuse burnper job), then confirm each board when asked; the agent reads the chip first (carries another serial: FAIL, set the board aside). This cannot be undone. Boards without auto-reset ask for the BOOT-hold and RESET again. When: production boards only; dev and bench boards stayefuse off(the default) and remain FLASHED;virtualrehearses the step on a file without touching the board. - When you are done, or step away, run
nova-station logout(dev and QA flavours; on production sign out in the console's station panel).
nova-station logout before you walk away.Device identity certificate and device map
Before pre-registration, the unit's Pi gets its identity and its device map in one job over the box's station door: nova-station cert pi --unit NVA-TTB-00002-X with this computer cabled to the box's board LAN (or image --unit right after imaging, or Install identity… in the console). The box makes its key (it never leaves the Pi) and signing request; the console's Device CA issues the certificate only for the Pi the Inventory lists as the unit's gateway; the box installs it; the console activates it (the unit's identity from then on); the console signs the unit's device map (the gateway Pi, each board's serial and Ethernet MAC, the mixer's MAC) and the box installs it; then the box shows both back and the agent compares them. The box serves only the boards its map lists. Pre-registration in the console then takes the activated identity — nothing to type — and the box connects to the cloud with it. The unit must be assembled (first identity) or in RMA (a swapped Pi, or a re-key with --rma). nova-station cert trust writes the root bundle an image bakes in. Needs the device.cert.issue permission.
Device EOL (whole-unit test)
After a unit is assembled, verified and pre-registered, and its box is online, run nova-station eol --unit NVA-TTB-00002-X. The console refuses a unit that is not pre-registered (“not_ingested”), a box that is offline (“device_offline”), a box whose identity or device map is not the Inventory's (“eol_blocked”, said in words with the job that clears it: identity pending — run the identity job, cert pi; map missing — the box reports no device map; map stale — the box runs map v2, the current one is v3: map install) or one that has not reported every channel of its config (“channels_missing”: a board is missing, unflashed or not announced). It then claims the unit as the factory tester account (an app user marked as such; by default the bootstrap app account, which has the bootstrap admin email and is created verified and marked factory tester when the backend first starts; the claim is a factory test, never an activation, and needs no label) and the agent walks the channel table: on and off, both ends and the middle of a level or colour range, each source, one pulse on a lock or a switch, a toggle twice; sensors are only read. Each command goes through the same path an owner's app uses and the box's real answer is shown. For every channel you are asked whether the load did what was commanded; answer Yes, No or Skip. At the end the verdict is written on the unit (ERP › Devices › the unit: EOL pass / EOL fail with the detail), the tester's claim is released, and with --factory-reset a passed box is reset to factory state first so it ships clean. After a repair (the unit in RMA, its identity and current device map on the box) the same command runs the EOL again; on an owned unit it is a service test: nothing is claimed, the commands go as you (actor service:<operator>) while the owner keeps the device, --factory-reset is refused (factory_reset_refused: a service test never wipes the owner's state), and the owner's app shows “Device tested”. The unit returns to service in the console only after a passed run. If the agent dies mid-run, the console abandons the run after 30 minutes idle and releases the claim; the unit keeps its previous result.
Prompts
The wording and the steps come from the revision's flash procedure in the console (revision page › Flash procedure; set from the firmware repo's revision.json). A revision without one gets the agent's built-in steps for its role. The procedure is kept beside the downloaded firmware, so --offline follows the same steps.
| Prompt | When | Do |
|---|---|---|
| Hold BOOT, tap RESET, release BOOT, then Continue | Power boards (no auto-reset circuit), or when the agent could not connect | Exactly that, then continue or retry |
| Press RESET on the board, then Continue | After writing a board the agent cannot reset | Press RESET, then continue. The agent then asks the board which firmware it runs |
| The board did not answer within N s | The board did not report its firmware | Close any serial monitor, press RESET or reseat the cable, retry once |
| Cannot open port | Wrong cable, or a serial monitor holds the port | Close the monitor, retry |
| Scan the board's sticker (or type its serial) | Before the write, unless --serial was given | Scan the QR. A wrong check character, an unknown serial or a board of another revision is reported and asked again |
| No chip id could be read from NVA-…. Record it as DEAD ON ARRIVAL — or skip? | The flash failed before the chip answered at all | skip (the default) if the cable or port may be at fault, fix it and flash again; record if the board is dead: its row turns FAULTY “dead on arrival” on its lot — set it aside for the supplier |
Options
| Flag | Meaning |
|---|---|
--port <device> | Pick the port. Without it the agent uses the single USB board it finds. |
--version x.y.z | Safety check: must equal the console's current version. |
--dry-run | Download and cache only. Nothing is written to the board. |
--offline | Backend unreachable: use the newest cached copy, with a warning. Bench use only. |
--release-file <file> --version x.y.z | Engineering only: flash a local factory image without asking the console. |
--notes "…" | Free text stored with the result. |
--serial NVA-PB-00007-4 | The board's serial from its sticker, given up front instead of scanning at the prompt. |
Where the result goes
Every attempt, PASS or FAIL, is recorded in three places. The Inventory board row is the one that counts: it has existed since the lot was received; the agent reports the scanned serial, the chip id it read, release, checksum and result, and the Inventory binds the chip id to the serial at the first flash — a different chip under that serial, or that chip under another serial, is refused (wrong sticker). A FAIL marks the board faulty; a later PASS before board EOL clears it. This needs the inventory.write permission on your session. Without it the agent warns and skips the row.
The console's station Activity tab records the attempt under your name. The station machine keeps a local copy in ~/.nova/station/boards.csv: time, station, port, MAC address, revision, release, checksum, result, banner version, seconds, notes, serial and lot. If the Inventory refuses a report (wrong revision, wrong sticker), the agent says so, keeps the local copy, and you flash again after fixing the cause.
Flash from the console
The same job without a terminal: a Flash… button on the console's ERP board page (serial prefilled from the sticker) and on the catalogue revision page. The console cannot reach USB itself, so the agent must run on the computer the board is plugged into:
nova-station config set console_origin https://<console> # once
nova-station autostart on # once: the OS runs the service at every login
nova-station serve # the same service by hand, for one sessionserve is the engine: the local service the dialog talks to. autostart on is the switch that makes the operating system run that engine at every login and restart it if it dies (a LaunchAgent on macOS, a logon task on Windows, a user unit on Linux); autostart off removes it; autostart status says whether it is registered and answering. From agent 0.6.5 the service re-reads its settings on every request, so a nova-station config set … or nova-station login in a terminal takes effect without restarting it.
- Open the board in the console and click Flash…. The dialog finds the agent on this computer. If none answers it shows the installer for your system and the three steps, and continues by itself once the service runs. A browser cannot tell "not installed" from "installed but not running"; if this browser has seen the agent here before, the panel says so and leads with
autostart on. - If nobody is signed in on the agent, one click (Sign in on this station as you) signs you in: no code to type, the console approves it for you. If someone else is signed in, the dialog signs them out first (recorded on the station) and then offers the same button. Signed in, the panel shows until when the sign-in holds (agent 0.6.6: the 12-hour end and the idle end); when the console has ended it, the panel says why and offers the same button. A job stopped because the sign-in ended mid-run says so and offers Sign in again as you; then start the run again.
- Check the serial, pick the USB port if more than one board is plugged in, click Start flashing. The log scrolls in the dialog; the agent's questions (hold BOOT, retry, dead on arrival) are buttons, a scan request is a text field.
- PASS or FAIL with the reason; the board row shows the chip id, firmware and condition at once.
Security in one line: serve listens on 127.0.0.1 only and answers the console origin only, with a custom header that forces a browser preflight other websites cannot pass; who may act is the operator session, never a pairing code (the old pairing command is gone).
Board EOL on the rig
What: the end-of-line test of one board, after it is flashed and before it can be fitted in a unit. The board runs its product firmware and the station machine acts as its supervisor on the bus, exactly as the Pi does in the box: no test firmware, no serial commands. Who: the operator at the EOL rig, signed in with inventory.write and inventory.test. When: after the serial step (stage SERIALISED), or after the flash on dev boards (stage FLASHED); the board must be at stage FLASHED, SERIALISED, EOL_FAILED or EOL_PASSED, and not sitting in a device. Where: the EOL rig; fixture, steps, pass criteria and timing per board model are in the board EOL rig procedure kept with the station tooling.
The rig
| Item | What |
|---|---|
| Station machine | Any machine with the agent (0.4.0 or later), signed in. |
| Rig network port | A dedicated Ethernet port (a USB-Ethernet adapter) cabled straight to the board, never the office network. |
| Bench supply | 24 V on the board's input, applied last (harness, then Ethernet, then the supply). Loads are judged on real 24 V only, never on USB power: USB back-feeds the rails. |
| Fixture harness | One plug-in harness per board model, with a known load on every output. |
| Meter | Optional; needed with --measure. |
| Sticker scanner | The same QR scanner as the flash station. |
The rig network: this computer is the board's gateway
What: the station machine's dedicated rig Ethernet port hands the board its address (DHCP) and is its router. Why: the product firmware dials ws://<its DHCP gateway>:9000/rpc (in the box the gateway is the Pi, 10.42.0.1); on the rig the agent listens on port 9000, so the gateway must be this machine. Without it the board gets no address and the run waits for its announce until it times out. How, once per machine:
| System | Set-up | Board dials |
|---|---|---|
| macOS | System Settings › General › Sharing › Internet Sharing (the ⓘ button): share your connection from Wi-Fi to computers using the USB-Ethernet adapter cabled to the board, then turn it on. On the first run allow incoming connections for nova-station when macOS asks. | 192.168.2.1:9000 |
| Linux | NetworkManager's Shared to other computers on the rig adapter (it takes 10.42.0.1/24), or a static 10.42.0.1/24 plus your own dnsmasq on it (commands below). | 10.42.0.1:9000 |
| Windows | Internet Connection Sharing from the adapter that has the internet onto the rig adapter (Control Panel › Network Connections › that adapter › Properties › Sharing); the rig adapter becomes 192.168.137.1. The firewall rule Nova Station board EOL (TCP 9000) comes with the setup program; if Windows still asks about nova-station, choose Allow, never Cancel (Cancel adds a block rule). | 192.168.137.1:9000 |
# Linux, either NetworkManager sharing …
nmcli connection modify <connection> ipv4.method shared ipv4.addresses 10.42.0.1/24
nmcli connection up <connection>
# … or a static address and dnsmasq on the rig adapter
sudo ip addr add 10.42.0.1/24 dev <nic>
sudo dnsmasq --interface=<nic> --bind-interfaces --dhcp-range=10.42.0.50,10.42.0.150,12h --dhcp-option=3,10.42.0.1One board on the rig at a time. Nothing else may listen on port 9000 on that machine (a supervisor, another eol board); the agent says so when it cannot listen. To see what holds the port:
lsof -nP -iTCP:9000 -sTCP:LISTEN # macOS / Linux: prints nothing = free
netstat -ano | findstr :9000 # Windows10.42.0.1); a second DHCP server on the same cable breaks both.nova-station eol board --serial NVA-PB-00007-4 # signed in: revision + expected firmware from the board row
nova-station eol board --serial NVA-PB-00007-4 --measure # also asks the meter reading at every switched connector
nova-station eol board --serial NVA-PB-00007-4 --offline --revision PB/M1/RevE --expect-fw 0.3.1 # bench, no consoleWhat it does
- Looks the sticker up in the Inventory: the board must be at stage FLASHED, SERIALISED or EOL_*, and not in a device.
- Listens on
0.0.0.0:9000and waits for the board's announce. Power-cycle the board after starting the run. - Checks the identity: model and hardware revision, firmware = the release of its last flash, the eFuse serial, and the Ethernet MAC = the one the board's firmware derives from the bound chip id (first byte with the local bit set and 0x04 flipped, last byte + 1: chip 38:44:be:ca:07:e0 announces 3E:44:BE:CA:07:E1). Then the power-on states.
- Drives every port as its capabilities allow: refusals, on/off, level sweeps (and the fan's metrics), colours, both pulse kinds with their measured length, the sensor's re-validation. For each port it asks you whether the load did it: Yes / No / Skip / Redo (Enter = Yes, or the first letter). Redo (agent 0.5.4) runs that port's commands again — the visible steps and the return to rest, not the refusals — and asks again, as often as you like; with
--measure, typingredoat the meter question does the same. Every attempt is kept in the result file; the verdict uses the last one and your final answer. A light port (the LED board, agent 0.5.8) holds each visible step 2 s and asks a colour challenge instead of Yes / No: the strip is lit in three colours drawn at random and you name the colour the whole strip shows — Red / Green / Blue / Warm / Cool, Mixed (not one colour along its length) or Dark, or Redo. The question never says the colour; one wrong name fails the port. - Any fault event fails its port. PASS needs every check and every port.
Where the result goes
A result file ~/.nova/station/board-eol/<serial>-<time>.json and a row in board_eol.csv. A board that never reaches the bus is a rig problem: nothing is recorded.
The verdict is reported to the board row (POST /station/inventory/boards/<serial>/eol, needs inventory.test): the row moves to EOL passed or EOL failed. A refused report (another revision, a different chip, an unconfirmed PASS in production) or an --offline run says not recorded centrally: then the file is the record; run it again, or staff enter the verdict with Record EOL… on the console's ERP board page.
Writing a gateway Pi
What: nova-station image writes a Pi card or disk with the console's current release: a sealed image that carries its own first boot, so nothing is staged on it. With --serial it first checks the sticker belongs to the MB model in --revision (MB/M1 gateway or MB/M2 kiosk + video, fixed when the lot was received) and stops before writing if not; after the first boot it records the board row, writes the board's identity file on the box (/var/lib/novabox/identity/board.json) and marks the board SERIALISED; with --unit it gives the unit its identity certificate (the image stages no seed: the unit's signed seed reaches the box by Push config on the unit page (gate 2) once the box is online, or at the bench by nova-station seed --unit and copying both files to /etc/novabox/). When: at the board stage (--serial) and again at assembly (--unit). Where: a macOS or Windows station (Linux too, from a checkout) with a card reader or a USB NVMe enclosure (or the Pi itself under rpiboot), cabled to the box's board LAN for the first boot. No checkout of the firmware repo is needed: provisioning from a checkout (--provision-src, or nova-station config set provision_src <checkout>/master_boards_Raspi) is a terminal-only engineering option. Who: the operator at the imaging station; writing a disk goes through the privileged writer helper (macOS: installed once, First-time setup; Windows: the service the setup program installed), the agent itself never runs as root or administrator. Status: built 2026-10-20, not yet proven on the bench.
nova-station targets # which disks may be written, and why the others may not
nova-station image --revision MB/M1/RevA --target /dev/disk4 # the console's current MB image release
nova-station image --revision MB/M1/RevA --target PhysicalDrive2 # the same on Windows (\.PhysicalDrive2; type PhysicalDrive2 back)
nova-station image --revision MB/M1/RevA --target /dev/disk4 --image-file novabox-mb-m1-reva-<V>-prod.img.xz --sha256 <its .json sha256> --no-provision # engineering: a local sealed image
nova-station image --revision MB/M2/RevA --target /dev/disk4 --serial NVA-MB-00002-0 [--unit NVA-TTB-00002-X]
nova-station image … --serial NVA-MB-00001-2 # board stage: the MB sticker → board row + SERIALISED
nova-station image … --unit NVA-TTB-00002-X # at assembly: the unit's identity certificate
nova-station cert pi --unit NVA-TTB-00002-X # identity + device map, for a box imaged earlier
nova-station cert pi --unit NVA-TTB-00002-X --rma "RMA-17: boot medium" # re-key an identified box (an RMA order)
nova-station map install --unit NVA-TTB-00002-X --rma "RMA-17: LED board" # board-only RMA swap: the new device map
nova-station map show --unit NVA-TTB-00002-X [--rma "…"] # what map the cabled box holdsWhat happens, in order
- Sticker check (
--serial). The Inventory row of the serial must be a board of--revision; otherwise the job stops before anything is written: “NVA-MB-… is a MB/M1/RevA board (its lot was received as that model), not a MB/M2/RevA — use --revision MB/M1/RevA or the right sticker; a board's model never changes”. An unknown serial stops too. - Release. The console's current release of the revision (downloaded once into the cache, checked before every use; the console hands the agent a 10-minute link to that one file in Nova's private bucket, which the agent fetches without its station key or sign-in and checks against the SHA-256 and size), or
--image-filefor engineering (--sha256refuses a different file)..img,.img.xz,.img.gz(and.zston Python 3.14 or later) are written decompressed. - Target. Refused, never just warned: a partition instead of a whole disk, the disk this computer runs from, an internal disk, a virtual disk, anything over 2 TB. Then you type the device name (
disk4); anything else stops before a byte is written.--confirm disk4types it up front. - Write and verify. The image is unpacked to a raw file in
~/.nova/station/cache/write/(hashed as it is written; it needs free space for its full size there and is removed after the job), then the writer helper (macOS:sudo -n /usr/local/libexec/nova-station-write write <raw> <disk>; Windows: theNovaStationWriterservice over its named pipe) re-checks the disk and the file, unmounts the drive (Windows: locks and dismounts every volume on it until the read-back is done), writes it and reads it back: a read-back hash that differs from the raw file's is a FAIL (bad card or reader). - Nothing staged. The sealed image carries its own first boot, so the card is ejected as written. A console release is never staged, from the console or the terminal, and no flag is needed (agent 0.8.1). Engineering, terminal only: a plain Raspberry Pi OS
--image-fileis staged from a checkout of the firmware repo (--provision-src <checkout>/master_boards_Raspiornova-station config set provision_src …);--no-provisionis only for an--image-filethat is itself a sealed image, and--no-provision,--provision-srcand--seedwithout an--image-fileto stage are refused. The staged payload goes on the boot partition: the box's set-up scripts and software, its hostname, user and cloud address, this station's ssh public key, the console's pinned root certificates (when signed in) and, with--unit(or--seed), the unit'sproduct_config.jsonandproduct_config.sigbeside it (the cloud's signature, checked asnova-station seeddoes;--seed FILEneeds FILE's.sigbeside it and is refused when the two do not belong together), set to run once at first boot. - First boot (unless
--no-wait). Put the card into the Pi, cable this computer to the box's board LAN (the Pi's Ethernet port or the box's board switch; you get a10.42.0.1xxaddress) and power on. The agent reaches the box atkiosk@10.42.0.1over ssh and waits for its first boot to finish (a checkout-provisioned card: each provisioning step; it takes tens of minutes).--boot-timeoutsets the wait in minutes. - Boot self-test. The box's services are enabled,
/datais mounted,eth0is10.42.0.1/24, the box software and pinned roots are present; the Pi's own serial number is read. - Board row (
--serial). The MB sticker is reported to the Inventory like a flash (chip idpi:<Pi serial>, the image release, PASS) → stage FLASHED. A refusal fails the job (the local row records it): fix the cause, image again. - Serial step (
--serial). The Pi has no Nova eFuse, so its serial and model go into its identity file on the data partition,/var/lib/novabox/identity/board.json(serial, model, revision, chip id, image, station, time; a factory reset never touches it), read back, then the board is marked SERIALISED. A board already SERIALISED (a re-image) keeps its stage. If the serial step is refused the job fails and names the reason (chip_bound_elsewhere= wrong sticker: set the board aside; see the messages table). - Identity and device map (
--unit), in this order: pre-check — the unit's signed device map from the console (POST /station/device-maps); its gateway chip must be the Pi on the cable, else “the Pi on the cable (pi:…) is not NVA-…'s gateway in the Inventory …: a replaced Pi is recorded in the console first — unit › RMA › swap the gateway (MB) board — then run this job again. Nothing was issued or installed.” ·identity.csr— the box makes its key and signing request (the key never leaves it) · issue — the console's Device CA (only for the Inventory's gateway chip) ·identity.install— the cloud signing keys and the chain, and the box's cloud link starts by itself · activate — the installed fingerprint becomes the unit's identity (POST /station/certificates/activate; any other certificate of the serial is revoked; the owner of a claimed unit is told it was repaired) · map — the map is fetched again and installed (map.install) ·identity.show+map.show— the fingerprint and the map version the box shows must be the ones issued. A re-run is safe (same key, same map version). Then Pre-register… in the console takes the activated identity, with nothing to type. - Record. A row in
~/.nova/station/images.csv.
nova-station cert pi --unit … runs only the identity step (with its map), for a box imaged earlier or a Pi swapped in RMA, with this computer cabled to its board LAN. Nothing is copied by hand. A box that already has an identity takes it only as an RMA order: --rma "<reason>" (a re-key of the same Pi, e.g. a replaced boot medium).
Device map after an RMA swap
What: the box serves only the boards its signed device map lists (serial and Ethernet MAC each; the mixer by MAC); any other board is refused (Foreign board on the console's device page), and the box takes a new map only from a station. When: after the console's Swap component… on a unit in RMA — always swap in the console first, then visit the station. How: a gateway (Pi) swap — cert pi --unit … (identity and map; the new Pi has no identity, so no --rma); a board-only swap (LED, power board, mixer) — nova-station map install --unit … --rma "<reason>" (the box is identified: an RMA order; the console's Install device map… runs the same). The map's gateway must be the Pi on the cable (the same pre-check). nova-station map show --unit … prints the map the box holds (version, gateway, each role's serial and MAC; --rma on an identified box). Then run its device EOL (still in RMA): it starts only when the box runs the Inventory's identity and current map; the console returns the unit to service only after it passed. Needs device.cert.issue.
Sign-in
The console release, --serial and --unit need a session (scopes: pi.image for the console release, firmware.read for the seed and roots, inventory.write + pi.image for --serial, device.cert.issue for --unit). A local --image-file with neither needs nobody: that is engineering, and nothing is recorded in the console.
The bench cloud by name (agent 0.8.3, ruled 2026-10-26)
A bench or dev image never carries an address: it names wss://dev-cloud.novacube.test:9443/uplink, and the station tells the cabled box where that name lives — this machine's IPv4 on the interface of its default route (the network the box's WAN port is on; never the board LAN 10.42.0.x). The console says whether it is the bench cloud; then the image job (after the first-boot self-test, before the serial step) and cert pi (before the identity) send the door operation cloud.locate themselves and wait until the box resolves the name there — the log says cloud: dev-cloud.novacube.test → 192.168.29.92 (station). Moved to another network (Wi-Fi instead of Ethernet, another desk)? Re-point the cabled box alone with nova-station cloud locate (or --address 192.168.29.92); any door state, scope pi.image. A box that is not a bench build refuses (not_bench: QA and production images resolve their public name themselves); on a QA or production console nothing is sent. The Pi EOL checks it (the cloud check). The station machine itself needs the hosts line of First-time setup, step 2.
Master board (Pi) EOL
What: the end-of-line test of a master board (the gateway Pi). No rig and no questions: the box checks itself and the station reads the answers over the same ssh link as the imaging boot self-test (kiosk@10.42.0.1, the station's own key). When: after nova-station image … --serial NVA-MB-… (stage SERIALISED, chip pi:<SoC serial>), with the box still cabled; again after a fix (stage EOL_FAILED or EOL_PASSED). Assembly refuses an MB board with no EOL (board_untested): this is the step that records it. Who: the operator at the imaging station, signed in with inventory.write (the row), firmware.read (the model profile) and inventory.test (the result). Where: this computer cabled to the box's board LAN (the Pi's Ethernet port or the box's board switch) with Internet Sharing off: the Pi is the gateway here. The box's WAN port in a network with DHCP.
nova-station eol pi --serial NVA-MB-00001-2 # the default EOL: never reboots the box
nova-station eol pi --serial NVA-MB-00001-2 --slot-drill # + the A/B slot drill: REBOOTS the box twice (about 2–5 min)What it does
- Looks the sticker up in the Inventory: an MB revision at stage SERIALISED or EOL_*, not in a device, bound to a Pi.
- Fetches the MB model's service profile and its fingerprint from the console.
- Reaches the box, which must be the Pi the row is bound to: another box is not tested and nothing is reported.
- Runs one read-only fact script on the box and turns the facts into the checks below. Every check is a gate; what is only informative rides in its detail.
- Reports the checks to the board row: the backend derives PASS → EOL passed (condition OK) or FAIL → EOL failed (faulty, failed end-of-line).
| Check | PASS when |
|---|---|
| identity file | The box's identity file carries this serial, the chip pi:<SoC serial> and the row's model. |
| boot medium | The sealed image's six partitions (autoboot, boot-a/b, rootfs-a/b, data) on one disk; the detail names NVMe, SD card or USB disk. |
| root slot | The kernel's root is rootfs-a or rootfs-b with the matching slot, booted, good and the primary: a committed boot, not a trial. |
| data partition | /data is the novabox-data partition, mounted noexec, nosuid, nodev. |
| board LAN (eth0) | Up with 10.42.0.1/24. |
| WAN (wan0) | Up with an IPv4 address: plug the WAN cable in. |
| dnsmasq | Active (the board LAN's DHCP). |
| BLE | Models with BLE only: the controller is powered. |
| root filesystem | Agent 0.8.2: a prod or rehearsal image runs on the overlay root (the booted slot read-only underneath, a tmpfs on top); a dev image runs its slot ext4 read-write. A prod box whose root is plain ext4 ro fails: the overlay did not engage (found on the bench 2026-10-26 — every prod image before 1.0.12). |
| RTC | The real-time clock is present and readable (the backup cell is ruled but not fitted on the bench Pis). |
| cloud | Agent 0.8.3: a bench image (image.json bench_egress: true) names its cloud dev-cloud.novacube.test and the box must resolve it to this station's default-route address — FAIL "resolves to X, this station is Y": run nova-station cloud locate. A QA or production image: PASS, information (its public name; FAIL only if it carries the bench name). The detail says what the box resolves the host to. |
| time sync | Agent 0.8.2: on a prod or rehearsal box with a WAN default route the clock must be NTP-synchronised (the agent re-reads for up to 120 s); without a WAN route, or on a dev image, it is detail only. |
| image | The image's model, revision, model profile fingerprint, services and proto match the console's model. |
| image release | The box runs the release the row was imaged with. |
| supervisor | The supervisor is active. |
| service ble / kiosk / video | The model's units are installed and enabled (they start once a seed lists them; a box before assembly has none). |
| no foreign services | No unit of a service the model does not promise is installed. |
| output HDMI-A-1 / DSI-2 | Each display output of the profile exists (DSI only with the image's panel overlay). |
| slot drill (tryboot) | --slot-drill only: A → B as a trial, committed, then back B → A the same way; ends on the slot it started on, both slots good. |
Why the slot drill is off by default: it reboots the box twice and commits each slot in turn. It runs only when every other check passed. Run it on the first box of a new image or NVMe batch, or when the update path is in question.
A result file ~/.nova/station/pi-eol/<serial>-<time>.json (the checks and every fact the box reported) and a row in pi_eol.csv. The run passes only when every check passed and the Inventory recorded it: an unrecorded PASS leaves the row untested and assembly still refuses it.
Board EOL, imaging and identity from the console
What: the same jobs as eol board, eol pi, image, cert pi and map install, started from the console instead of a terminal, through the same local service as Flash… (this computer only, the console address only, the job runs as the person signed in on the agent; the dialog signs you in the same way). Needs: agent 0.5.0 or later (local-service protocol 3; Run Pi EOL… needs 0.5.3); an older agent gets too old and the installer. One at a time: one board EOL, one imaging job and one Pi EOL per station, because the rig's port 9000, the card reader and the board-LAN cable are single; a second start says the job is already running.
Run board EOL…
ERP › Boards › a power or LED board › Actions › Run board EOL… (console permission inventory.test; the agent's sign-in needs inventory.write to look the board up and inventory.test to record the verdict). Serial and revision come from the page. Options: Ask the meter reading (--measure) and Bus evidence only (--skip-confirm, marked unconfirmed; a production backend refuses such a PASS with unconfirmed_pass, so it is for bench work only). The form opens with Before you start for the system the agent runs on: this computer is the board's gateway on a dedicated Ethernet port (what, why, the set-up steps, the board's address and the port 9000 check, as in The rig network), the harness and 24 V supply (never USB power), and the reminder to turn Internet Sharing off again and re-cable before imaging or testing a box. Start, then power-cycle the board on the rig. The dialog shows one row per port, Waiting → Testing → Pass / Fail / Skipped with the failing check; the question for the port under test as Yes / No / Skip / Redo (an LED board port: the colour challenge, “which colour is the whole strip showing?” with Redo, Red … Dark) with what to look at (Redo, agent 0.5.4: runs this output's commands again so you can watch the load); the meter question as a volts field with Skip and Redo. Between two questions the question box never empties: the buttons grey out the moment you answer, and in the same place (same height, no jump) the dialog says what it recorded — “Recorded: Port C — Yes”, “Recorded: Port D — 12.0 V” or “Redo: running Port C again” — then, beside a spinner, what the run is doing now: “Testing Port D (load E, J13): …” with that port's latest step from the log (refusals, the visible steps, back to rest) and its live state; after the last port “All ports done — computing the verdict…”. The next question replaces it in place with the focus on its first button (the volts field for the meter). Then the verdict, and whether it was recorded on the board row or only in the result file on the station (offline, or refused: the reason is shown).
Every question about a port says what its load should be doing: the operator question, the meter question and the run after a Redo show the port's description (the same text the operator question asks about, plus its on/off sequence from agents that send it). The meter question adds The output is ON now — read the voltage at <connector>; expected about <rail> V.
Run Pi EOL…
ERP › Boards › an MB board › Actions › Run Pi EOL… (console permission inventory.test; the agent's sign-in needs inventory.write, firmware.read and inventory.test; agent 0.5.3 or later, an older one is named and Start Pi EOL stays off). It replaces Run board EOL… on an MB board and is greyed out with the reason unless the board is at stage Serialised, EOL failed or EOL passed and not in a device. Serial and revision come from the page. Before you start: this computer cabled to the box's board LAN with Internet Sharing off, the box powered and imaged with this serial, the WAN port in a network with DHCP. Options: Slot drill (reboots the box twice), off by default (--slot-drill), and Notes. The dialog shows the steps (Board row › Model profile › Reach the box › Checks › Slot drill › Record › Verdict), then one row per check with Pass / Fail and what was seen, the verdict (“n of m checks passed”) and whether it was recorded on the board row; a run that stopped early (wrong box, not reachable) says why and records nothing.
Image a Pi…
ERP › Boards › an MB board: Image a Pi… is the page's main button there (an MB board is a Pi: imaged, never flashed). ERP › Devices › a unit › Actions › Image the Pi… images the unit's MB board and installs its identity after the boot (the unit must be assembled or in RMA; its seed then goes down with Push config on the unit page, gate 2, once the box is online). The dialog lists the disks the agent sees: writable ones to choose, refused ones greyed with the reason (the same rules as nova-station targets). Type the chosen disk's name (disk4; Windows PhysicalDrive2) before Erase and write enables; the agent checks the typed name again. Only the console's current release of the revision is written: an engineering image file stays a terminal job. The steps tick through (Release, Target, Write with the bytes written, Verify, Eject, First boot, Self-test, Board row, Identity); at First boot the dialog asks you to move the card into the Pi, cable this computer to the board LAN and power on (Continue / Abort). Untick Wait for the first boot to write and verify only.
GET /targets reports helper_ready and, when not ready, helper_problem ending with the install command): First-time setup of a station machine, step 1. Until then the dialog says so and the job stops before writing. Nothing else is needed: from agent 0.6.5 an image job from the console never stages provisioning (the console's release is the sealed image, which carries its own first boot), so an installed agent (the macOS .pkg or the Windows setup program) images from the console without any provision_src. Provisioning from a checkout (--provision-src, config set provision_src) is a terminal-only engineering option.Install identity…
ERP › Devices › a unit › Actions › Install identity… (permission device.cert.issue; agent 0.8.0 or later): cert pi for a box imaged earlier or a Pi swapped in RMA. Cable this computer to the box's board LAN; the pre-check, the key and signing request, issue, install, activate and the device map run as in Writing a gateway Pi, and both fingerprints and the map version are compared. On a unit in RMA the dialog has an optional RMA reason (re-key only) for a box that already has an identity. The fingerprint and the map version are shown at the end, with a copy button; Pre-register… then shows the activated identity.
Install device map…
ERP › Devices › a unit in RMA › Install device map… (permission device.cert.issue; agent 0.8.0 or later; job kind map_install): map install after a board-only swap. Type the RMA reason (required), cable this computer to the box's board LAN, Install device map; the result names the map version in force.
When a run does not finish
What: a job started from the console lives in the agent on this computer, not in the browser tab. Agent 0.5.6 (local-service protocol 4) lets a page find its run again, cancel it, and keeps the verdict when it cannot be recorded at once. Who: the operator at the station. Where: every station dialog (Flash…, Run board EOL…, Run Pi EOL…, Image a Pi…, Install identity…) and the ERP board and unit pages.
| What happened | What the station does | What the console shows |
|---|---|---|
| The tab or window was closed while a question waited | The run keeps waiting. After 15 min without an answer it cancels itself (reason: no answer for 15 min). The limit is the setting prompt_idle_s (60 … 86400 s). | Reopening the dialog re-attaches to the run, questions answerable again. Closing a dialog while its run goes asks first: Stay here · Leave it running · Cancel it. |
| The page was refreshed mid-run | Nothing changes. | The dialog's address reopens the same dialog, which re-attaches (no 'already running', no empty form). If the address lost the dialog, the board page shows: a … run for this board is in progress on the station — Open it. |
| You signed out of the console mid-run | Nothing: the station's sign-in is separate, the run goes on as the station's operator. | After you sign in again, the board page and the dialog find the run. |
| The station's sign-in ended mid-run (30 min idle, 12 h, nova-station logout), or the backend was down when the verdict was reported | A board EOL or Pi EOL verdict is kept on this station (~/.nova/station/outbox/) and sent again at the next sign-in, when the agent starts and every 5 min while someone is signed in — only as the operator who ran the test. A refusal on the merits (e.g. the chip differs) is final and not kept. | The verdict says: kept on this station, will be recorded when … is signed in and the console answers. |
| The agent restarted or crashed, or the computer slept or rebooted mid-run | The run is gone; nothing is recorded for it. The next agent says so once. | The station restarted during the run on <serial>; that run was lost — start it again. |
| The board lost power or the cable came out | The board left the bus during port X; the remaining ports are not tested; FAIL, recorded. | The verdict, as for any FAIL. |
| The same run open in two tabs | Each answer names its question; the second click on the same question is refused. | Answered in another window — the view shows where the run is now. |
Cancel run
Every running dialog has Cancel run (it asks first). The run stops at its next safe point: a question, the start of a port, the wait for the board, between steps. What a cancelled run leaves behind:
- Board EOL: the rig's port 9000 is freed; the result file and
board_eol.csvsayCANCELLEDwith the reason and the ports done so far; nothing is recorded on the board row — it keeps its stage. - Pi EOL: the same, in
pi_eol.csv. - Flash: nothing reported to the Inventory;
boards.csvand the console's Activity say CANCELLED. Cancelled after the write: the board is written but not verified — flash it again. - Pi imaging: no board row;
images.csvsays CANCELLED (cancelled at the boot question: the card is written and staged).
Cancel run is greyed, with the reason beside it, while a cut would do harm: while a flash or a card is written (a half-written flash or card would not boot), during the Pi slot drill, and once the verdict is being recorded (the report wins; there is no half state).
The outbox
nova-station outbox # the verdicts kept on this station
nova-station outbox send # send them now, as the signed-in operator
nova-station outbox drop <id> # discard one (its result file stays)
nova-station config set prompt_idle_s 1200 # a 20 min question limit for console runsThe backend records an EOL report as often as it is sent, so before sending a kept verdict again the agent reads the board row and drops it when the row already carries it (the case of a report the backend took whose answer never arrived). A terminal run (nova-station eol board …) has no question limit: the person at the terminal is there, and Ctrl-C ends it.
Building and publishing a release
Who: the engineer who owns the board's firmware. Where: the firmware repo (nova_boards_firmware), from its top folder. When: after the change is committed and tagged.
1. Build: tools/release.py
git tag PB-M1-RevE-0.3.1
tools/release.py --project power_board/PB/M1/RevE/firmware --env pb_m1_reve --version 0.3.1It leaves three files beside the release record in …/RevE/releases/0.3.1/:
| File | Purpose |
|---|---|
PB-M1-RevE-0.3.1.factory.bin | The one file uploaded to the console. It is flashed at address 0 on a blank board. The console refuses anything that is not a factory image. |
PB-M1-RevE-0.3.1.bin | The update image: the same bytes as the factory image from offset 0x10000. You never upload it — the console cuts it from the factory image at upload, records its checksum and serves it as the release's OTA image under this name. |
PB-M1-RevE-0.3.1.symbols.zip | Debug symbols, for reading a crash report later. |
The image carries NOVA PB/M1 RevE 0.3.1 g<commit> <date>. The board prints it at start-up, so any board names the exact source it runs. -dirty after the commit means uncommitted changes, and such a build is never released.
2. Publish: tools/catalogue_sync.py
export NOVA_CONSOLE_URL=http://localhost:8080 NOVA_STAFF_EMAIL=you@novacube.io # password is prompted
tools/catalogue_sync.py plan power_board/PB/M1/RevE --release 0.3.1
tools/catalogue_sync.py apply power_board/PB/M1/RevE --release 0.3.1 --publish --make-currentplan only reports. apply creates what is missing: the role, the model with its ports, the revision with its hardware letter and its flash procedure (the flash section of revision.json: chip, whether download mode is automatic, the BOOT-hold text, what to press after writing, the identity check and its timeout, bauds, the app offset, notes), and the release. It needs an ADMIN account. It refuses a build marked -dirty or whose checksum differs from the record. It reuses what already exists and updates a flash procedure that differs from the file. It never rewrites a model whose ports are frozen; a different set of ports is a new model.
The console route works too: on the revision page, upload the .factory.bin file, publish, make current. The script adds the checks above, so prefer it.
Every command
Run these in a terminal on the station machine, with your flavour's program name (nova-station-dev, nova-station-qa, nova-station; see Flavours for which commands each one has). <program> <command> --help prints the same. "Sign-in" means you must be signed in with nova-station login. "Scope" is the station permission the command needs.
| Command | What it does | Options | Sign-in · scope |
|---|---|---|---|
--version | Prints the agent version. | — | no |
doctor | Checks the machine: the flavour, its console (built in or set), settings folder and port; esptool, serial library, station key, who is signed in and what they may do, backend reachable, serial ports; the console's cloud (agent 0.8.3: on the bench cloud "bench name: dev-cloud.novacube.test → 127.0.0.1 (this machine)" or "!! bench name (to do): …" with the hosts line to add, First-time setup step 2). | — | no |
cloud locate | Dev flavour, bench console only (agent 0.8.3): tells the cabled bench box where dev-cloud.novacube.test lives — one order (cloud.locate + provision.status), any door state, no RMA; waits until the box resolves the name there and prints "dev-cloud.novacube.test → <address> (was …)". The image job and cert pi do the same by themselves on a bench console; use it after moving the station to another network. On a QA or production console it says so and sends nothing; a field image refuses (not_bench). | --address (default: this machine's IPv4 on its default route; never 10.42.0.x or loopback) · --host (the door, default 10.42.0.1) | yes · pi.image |
login | Signs you in: prints a link and a code, waits for your approval in the console, then shows what you may do. | Ctrl-C cancels | the console refuses without an allowed scope |
whoami | Who is signed in, on which station, until when, and what they may do. | — | yes |
logout | Dev and QA flavours: ends your session. The station cannot flash until someone signs in again. The production nova-station has no logout: sign out in the console's station panel. | — | — |
ports | Lists serial ports, to find the board. | — | no |
revisions | Lists board revisions and the current release of each. | — | yes · firmware.read |
flash | The full job: current release, cache, scan the board's sticker, write, prompts, the board reports its firmware, PASS or FAIL. The Inventory board row (chip id bound to the serial), the console's Activity and the local CSV. | --revision (required) · --port · --serial (the sticker's serial, else scanned at the prompt) · --version (must equal the current release) · --release-file (engineering, no Inventory row) · --offline · --dry-run · --notes · --efuse off|virtual|burn (the eFuse serial step for this job; default the station's efuse setting: off; burn burns the serial after a passed flash, reads it back and marks the board SERIALISED — a refusal fails the job) | yes · board.flash + firmware.read, inventory.write for the board row and the serial step |
check | Read-only: asks the board which firmware it runs and compares with the current release. Writes and records nothing. | --revision (required) · --port · --timeout seconds (default 12) | optional · firmware.read for the comparison |
eol | The device EOL (whole-unit test): the console claims the pre-registered unit as the factory tester, the agent sends every channel the commands its capabilities call for and shows the box's real answer, asks you per channel whether the load did it (Yes = PASS, No = FAIL, Skip = not checked), then finishes: the claim is released and the verdict lands on the unit's ERP page under your name. A failed or skipped channel makes the run FAIL. Result file under ~/.nova/station/eol/ and a row in eol.csv. | --unit (required; the unit's serial) · --skip-confirm (cloud answers only; the run is marked unconfirmed) · --factory-reset (a PASSED box is factory-reset before release) · --notes | yes · device.test |
eol board | The board EOL (see Board EOL on the rig): one flashed or serialised board (stage FLASHED or SERIALISED) on the EOL rig, this machine acting as its supervisor on the bus. Checks the announce (model, hardware revision, firmware = its last flash, serial, Ethernet MAC = the firmware's derivation from the bound chip) and the power-on states, then every port per its capabilities: refusals, on/off, level sweep (and the fan's metrics), colours, pulse edges and length, the sensor's re-validation; any fault event fails its port; asks per port whether the load did it (Yes / No / Skip / Redo — Redo runs that port's commands again, then asks again). Result file under ~/.nova/station/board-eol/ and a row in board_eol.csv; the verdict is recorded on the board row (refused when the rig ran another revision, the chip differs, or — in production — a PASS was unconfirmed). | --serial (the sticker; scanned at the prompt when omitted) · --revision (from the board row when signed in; required with --offline) · --measure (ask the meter reading at every switched connector, ±5 % of the rail) · --skip-confirm (no operator prompts: bus evidence only, marked unconfirmed — a production backend refuses its PASS: unconfirmed_pass) · --expect-fw (default: the release of the board's last flash) · --listen (default 0.0.0.0:9000) · --announce-timeout seconds (default 120) · --offline (bench: no Inventory lookup, no report) · --notes | yes (not with --offline) · inventory.write + inventory.test |
eol pi | The master board (Pi) EOL (see Master board (Pi) EOL): over the board-LAN ssh link the box checks itself against its MB model's service profile — identity file, boot medium, root slot, data partition, eth0 10.42.0.1/24, wan0 with an address, dnsmasq, BLE (models with BLE), RTC, the image against the console's model and the row's release, the supervisor, the model's service units and no other model's, each display output. Every check is a gate; reported to the board row → EOL passed / EOL failed. Result file under ~/.nova/station/pi-eol/ and a row in pi_eol.csv. | --serial (the sticker; scanned at the prompt when omitted) · --slot-drill (also the A/B tryboot drill: reboots the box twice; off by default) · --host (default 10.42.0.1) · --user (default kiosk) · --notes | yes · inventory.write + firmware.read + inventory.test |
seed | Downloads a unit's Pi seed with the cloud's signature: two files, product_config.json and product_config.sig. Nothing is written if the signature is missing or does not match the seed, the unit or the config version. Copy both files to the Pi's /etc/novabox/ — the box refuses a seed without its signature. For a bench Pi seeded by hand; production sends the seed with Push config on the unit page (gate 2) once the box is online — the sealed image stages no seed. | --unit (required; the unit's serial: --unit NVA-TTB-00002-X writes ./product_config.json + ./product_config.sig) · --out (--out /tmp/seed.json writes /tmp/seed.json + /tmp/seed.sig) · --force (overwrite either) | yes · firmware.read |
cert trust | Writes the root bundle (main + backup root) an image pins, and the cloud signing keys beside it. (cert issue, a CSR file in and a chain out, is gone since 0.8.0: an identity is only installed with its activation and its device map, by cert pi.) | --out (default nova-root-ca.pem) · --force | yes · firmware.read |
cert pi | The identity job over the board LAN: pre-check (the unit's device map: its gateway must be the Pi on the cable, else a refusal naming the console's gateway swap, nothing issued) → the box makes its key and signing request → the Device CA issues → the box installs → the console activates the installed fingerprint → the device map is installed → the box shows both and they are compared. Nothing is copied by hand. | --unit (required) · --rma (the reason, for a box that already has an identity: a re-key) · --host (default 10.42.0.1) · --port (default 9100) | yes · device.cert.issue |
map install | A board-only RMA swap: the unit's newly signed device map onto the cabled, identified box (an RMA order), same pre-check; then map.show must report it. | --unit (required) · --rma (required: the reason) · --host · --port | yes · device.cert.issue |
map show | Prints the device map the cabled box holds: version, unit, gateway (serial and chip), each role's serial and MAC; or that none is in force and why. | --unit (required) · --rma (on an identified box) · --host · --port | yes · device.cert.issue |
targets | Lists the disks of this computer a Pi image may be written to, and the refusal reason for every other one (internal, system disk, virtual, too large). | — | no |
image | Pi imaging (see Writing a gateway Pi): release, target refusals and typed confirmation, write, read-back verify, eject (a console release is a sealed image with its own first boot; nothing is staged, from the console or the terminal — agent 0.8.1; only an engineering --image-file is staged), then (waiting) first boot over the board LAN, boot self-test, MB board row, serial step (identity file, SERIALISED), identity certificate; a row in images.csv. With --serial the sticker is checked against --revision before anything is written. | --revision (required) · --target (required; the whole disk) · --confirm (the device name, typed up front) · --image-file (engineering: a local image) · --sha256 (expected hash of --image-file) · --version (must equal the current release) · --unit (identity after the boot) · --serial (MB sticker: checked against --revision before writing; board row + serial step after the boot) · --seed (an --image-file being staged only: a local seed, its .sig beside it) · --provision-src (engineering, with --image-file only: stage from a checkout's master_boards_Raspi/) · --no-provision (with --image-file only: the file is itself a sealed image; a console release never stages) · --hostname (default novabox) · --cloud-url · --host (default 10.42.0.1) · --user (default kiosk) · --no-wait (write and verify only) · --boot-timeout minutes (default 40) · --notes | yes, except a local --image-file without --unit/--serial · pi.image (console release), firmware.read (seed, roots), inventory.write + pi.image (--serial), device.cert.issue (--unit) |
cache ls | Lists releases kept on this machine. | — | no |
cache prune | Deletes cached releases that are no longer published. | — | yes · firmware.read |
config show | Prints the settings, with the key and session hidden. | — | no |
config set <key> <value> | Changes a setting: console_url (backend address), console_origin (the console's web address serve answers), station_key (this machine's key; setting it signs the operator out), station (a local name), cache_dir, records_csv, efuse (off by default · virtual · burn: the eFuse serial step of every flash on this station), prompt_idle_s (default 900: a console run whose question nobody answers this long cancels itself; 60 … 86400), provision_src (engineering, terminal imaging only: a checkout's master_boards_Raspi/). From agent 0.6.5 a running service picks the change up at its next request, no restart. | key and value | no |
outbox | The board EOL and Pi EOL verdicts kept on this station because their report could not be delivered (backend down, the station's sign-in ended mid-run). ls lists them (who ran it, attempts, last error); send sends now as the signed-in operator (only that operator's; a row that already carries the verdict is not sent twice); drop discards one (its result file stays). Sent by itself at sign-in, at agent start and every 5 min while signed in. | ls (default) · send · drop <id> | send: yes · inventory.test |
autostart on / off / status | on: registers serve with the operating system so it runs at every login and restarts if it dies, and starts it now (needs console_origin set). off: stops it and removes the registration. status: registered or not, and whether a service answers on this computer. | on · off · status | no |
serve | Starts the local service the console's Flash…, Run board EOL…, Run Pi EOL…, Image a Pi…, Install identity… and Install device map… dialogs drive (see Flash from the console; board EOL and imaging need agent 0.5.0, Pi EOL 0.5.3, identity and device map 0.8.0). Listens on this computer only; answers the console address only, with a header other web pages cannot send. | --port (default: the flavour's port — 8765 dev, 8766 QA, 8767 production; the console looks there) · --origin (the console address allowed to call it; repeatable; default: the console_origin setting, else localhost:3000) | jobs: as for flash; sign-in happens from the dialog |
Answers to prompts: Enter picks the first option, or type any unique first letters: c continue, a abort, r retry, s skip.
Files, cache and troubleshooting
Files on the station
| Path | What |
|---|---|
~/.nova/station/agent.json | Settings, the station key and the live operator session (readable by the owner only) |
~/.nova/station/cache/<role>/<model>/<rev>/<version>/ | Downloaded releases, named by checksum, each downloaded once per machine |
~/.nova/station/boards.csv | One row per flash attempt, the local copy of the Inventory board row (serial and lot included). An older 11-column file is moved to boards.v1.csv |
~/.nova/station/procedures.json | Optional per-revision overrides of the prompts |
~/.nova/station/board-eol/ | One result file per board EOL run, plus board_eol.csv |
~/.nova/station/pi-eol/ | One result file per Pi EOL run (the checks and every fact the box reported), plus pi_eol.csv |
~/.nova/station/images.csv | One row per Pi imaging run: time, station, operator, revision, release, checksums, target, bytes, serial, unit, Pi serial, fingerprint, result, seconds, notes |
~/.nova/station/pi_ed25519 | The station's own ssh key for reaching a box on its board LAN (authorised on the box by provisioning) |
The paths are the production flavour's; the dev flavour keeps the same files in ~/.nova/station-dev/ and QA in ~/.nova/station-qa/. NOVA_STATION_HOME moves all of it (dev flavour only). The cache is checked against the console's checksum before every use. A damaged copy is downloaded again, on a fresh 10-minute link to the file in Nova's private bucket (agent 0.6.3 and later; the link needs no station key and expires on its own). nova-station cache ls lists it, and nova-station cache prune removes releases that are no longer published.
Messages
| Message | Meaning and fix |
|---|---|
| nobody is signed in on this station | Run nova-station login and approve the code in the console. |
| your station session has ended | It expired, you signed out, or an admin signed you out. Sign in again. |
station_key_invalid | The key is mistyped, or the station was revoked. Ask an admin. |
| not allowed: board.flash … | Your account or this station lacks that scope. An admin grants it on your user page (Station permissions) or allows it on the station page. |
console_unreachable | Wrong backend address or no network. Run doctor. |
no_current_release | Nothing is published and current for that revision yet. |
revision_not_found | The console has no such revision. Sync the catalogue first. |
download sha256 … != catalogue | The stored file does not match its record. Stop and tell engineering. |
wrong image is running | The write did not take. Repeat the BOOT steps. |
| macOS asks to let Python find devices on the local network | Deny when the backend is on this machine. Allow when it is elsewhere on the network. |
-dirty | The image was built from uncommitted changes. Rebuild from the tag before releasing. |
| REFUSED: /dev/diskN is … (image) | The target is internal, the system disk, a partition, virtual or over 2 TB. Run nova-station targets and pick the card or enclosure. |
| the writer helper /usr/local/libexec/nova-station-write is not installed … (image) | macOS: open the .pkg again (administrator password). Linux or a pipx install: sudo sh <checkout>/station/installers/install-helper.sh, once (First-time setup, step 1). |
| … is 'nova-station-write 0', this agent needs 'nova-station-write 1' / not installed as root-owned 0755 | The helper is older than the agent or was changed: open the .pkg again (macOS), or run install-helper.sh again. |
| console imaging stops before writing: writing a disk from the console needs the writer helper and its sudoers rule …; the target list shows helper_ready false | The background service has no terminal for a password. Run First-time setup of a station machine, step 1 (it writes the rule), or image from a terminal. |
| sudo refused (terminal image, no sudoers rule) | The administrator password was wrong or this user may not use sudo; or install the helper's rule (the .pkg on macOS, install-helper.sh on Linux) so no password is needed. |
| Windows: writer helper: NOT READY — the writer helper service NovaStationWriter is not installed / not running / admits another user (image) | Run the Windows setup program again, as administrator, signed in to Windows as the station user: it (re)installs and starts the service for you. |
| Windows: You need to format the disk in drive X: before you can use it (after a write) | Windows does not read the Pi's Linux partition. Always Cancel, never Format; the job goes on by itself. |
| /dev/diskN: refused before writing: … | The writer helper's own check refused the disk or the image (the reason follows). The agent's checks passed, so tell engineering. |
| could not unpack … into ~/.nova/station/cache/write | Not enough free space for the raw image: free some (nova-station cache prune) and run again. |
| a board_eol / image / pi_eol job is already running on this station | One rig, one card reader and one board-LAN cable per station: let the open job end first (it may be in another tab). |
| the board EOL run waits for the board's announce (Waiting for the board to dial this computer…) | The board has no gateway on port 9000. Internet Sharing (ICS, NetworkManager sharing or dnsmasq) is off; or it shares to the wrong adapter (not the one cabled to the board); or port 9000 is busy (lsof -nP -iTCP:9000 -sTCP:LISTEN: a supervisor or another eol board holds it). Also check the 24 V supply and power-cycle the board. See The rig network. |
| eol pi: the box on the cable is Pi …, but NVA-MB-… is bound to pi:… | Another box is cabled, or the sticker belongs to another Pi: nothing was tested. Cable the right box or set the board aside. |
| eol pi: … is at stage FLASHED / RECEIVED: image it first | The row has no serial step: image it with --serial (Image a Pi… on its ERP page); that writes the identity file and records SERIALISED. |
| eol pi: … not a master board / eol board: master_board_eol | The two EOLs do not cross: an ESP board goes on the rig (eol board), a Pi runs eol pi. |
| eol pi: kiosk@10.42.0.1 does not answer | Cable this computer to the box's board LAN, Internet Sharing off on that adapter, power the box and wait for the boot. |
| eol pi: FAIL WAN (wan0) | The USB WAN adapter is missing (no wan0), or no DHCP on the WAN cable: plug the WAN side into a network with DHCP and run again. |
| eol pi: FAIL BLE | On the box: sudo rfkill list, systemctl status bluetooth hciuart. A Pi whose controller never appears is faulty. |
| eol pi: FAIL image (model_profile … ≠ the console's) | The card carries an image built from another catalogue state than the console's model: build and release the image again, re-image. |
| eol pi: FAIL image release | The box runs another release than the row was imaged with (an update, another card): re-image with --serial. |
| eol pi: FAIL output DSI-2 | An MB/M2 image without the panel overlay, or the panel cable: ls /sys/class/drm/ on the box. |
| eol pi: FAIL root slot … a trial (tryboot) boot | The box sits on an uncommitted trial slot (an interrupted drill or update): reboot it and run again. |
| eol pi: FAIL slot drill (tryboot) | The detail names the leg: did not go down (tryboot refused: journalctl -t novabox-ota), did not come back, or booted the wrong slot. The board is faulty until the update path is understood. |
| eol pi: NOT recorded: … not_serialised / chip_mismatch / … | The Inventory refused the result (the kind says why); the result file keeps the run and the job fails because the row is still untested. |
| VERIFY FAILED … bad card or reader (image) | If it happens right after a write on macOS, update the station agent (0.5.2 or newer blocks the OS from mounting the written drive); otherwise replace the card or reader. |
| read back stopped … No such file or directory (image) | The drive dropped off the bus during the read-back (bench 2026-10-23: a USB stick behind a hub, after a 22 GB write). Plug it into a port directly on the computer, not a hub; replug and run the job again. |
| kiosk@10.42.0.1 does not answer / the box never answered | Cable to the box's board LAN (not the venue network), check power and that the card is in the Pi. On the box: journalctl -u novabox-provision. |
| the console sent no signature (envelope) / seed_hash_mismatch: the signed seed_sha256 is not the sha256 of the seed sent beside it / the signature is for NVA-…, not … / sha256 … != the console's (seed, image) | The console's answer does not hold together: nothing was written. Report it (the box would refuse such a seed). |
| product_config.sig not found (image --seed) | A local seed is staged only with its signature beside it: nova-station seed --unit … writes both. |
| … refused the serial step — <kind>: … (flash with --efuse burn, image --serial) | not_flashed: flash or image first · already_tested: past its serial step · board_in_device / board_out_of_service: not at the board stage · serial_bound_to_other_chip / chip_bound_elsewhere: wrong sticker, set the board aside · wrong_serialise_method: the row's class takes the other step · scope_missing: ask for board.flash (eFuse) or pi.image (Pi). |
| the Pi on the cable (pi:…) is not NVA-…'s gateway in the Inventory (cert pi, image --unit, map install) | The Pi was replaced but the console does not know yet: on the unit's ERP page, RMA… then Swap component… on the MB board, then run the job again. Or another box is cabled. Nothing was issued or installed. |
| the backend made no device map for NVA-… — map_incomplete / unit_not_ready | map_incomplete: a role of the unit's config has no component, a board has no chip bound yet, or the mixer part has no MAC (the problems are listed). unit_not_ready: the unit is neither assembled nor in RMA. |
| box … has no identity yet: its device map comes with the identity job (map install) | A box without an identity gets its map from cert pi (or Install identity…), never alone. |
| console refused to start: … identity pending / reports no device map / runs device map v2, the current one is v3 (eol --unit) | eol_blocked: run cert pi (identity pending) or map install (map missing / stale) at a station, then start the EOL again (it runs in RMA). |
| provisioning failed at step … | On the box: journalctl -u novabox-provision and cat /var/lib/novabox/provision.json. Fix, reboot: the box retries. |