Nova documentation

Partner API · Console · Mobile app · Station agent · v2 · 2026-10-18

Staff console →

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 URLFormatAuth
https://api.novacube.io/v1JSON over HTTPS; snake_case keys; ignore unknown keysAuthorization: Bearer nova_pk_…
Local development: run the Nova backend locally and use http://localhost:8080/v1. The production host above is reserved and goes live with the first deployment.

Getting started in five calls

  1. 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.
  2. Check the key: GET /v1/key.
  3. List your devices: GET /v1/devices.
  4. Discover what a device can do: GET /v1/devices/{device_id}/channels.
  5. Command it: POST /v1/devices/{device_id}/commands, and read it back with GET …/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.

ArtifactWhat
com.nova.iot:nova-iot-sdkThe client. No Spring: the JDK's HTTP client and Jackson 2 (jackson-databind) only
com.nova.iot:nova-iot-sdk-spring-boot-starterOptional, 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.cmd

Then 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());
}
  • NovaClient is 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 as getSerial(), Instant times, unknown fields ignored). Every id is a java.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
}
Availability: the first release, 0.2.0, is being prepared. Ask Nova support for the bundle; the code on this page does not change.

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_Vq0b3Jq1Zt8rYw2nXc5LmP7dHs9KfA4e

Scopes 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.

ScopeAllows
readGET devices, channel discovery, state, GET …/stream, GET …/kiosk
commandPOST 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:

StatuscodeMeaning
401api_key_requiredNo bearer token
401api_key_invalidMalformed, unknown, or wrong secret
401api_key_revokedRevoked by the owner or by Nova; create a new key
401api_key_expiredA retiring key past its date; use its successor
403insufficient_scopeThe key lacks the scope this endpoint needs
403partner_suspendedThe owner suspended all keys (panic button); they can resume in the app
403developer_access_revokedNova suspended the partner surface of this account; contact support
429rate_limitedPer-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_revoked until 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
}
FieldMeaning
device_idStable id; the one you persist and key automation on
serialThe label serial; convenient for humans, do not derive anything from it
onlinePresence = the device's live connection to Nova. Commands need it; reads do not
last_seen_atLast time the device talked to Nova; null before its first connection
added_atWhen the device joined this account
blockedBlocked by Nova (support action). Commands answer 409 device_blocked until lifted
control_modeThe 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" } }
]
  • constraints are 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.
  • state is 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.

CapabilityFieldTypeConstraintValid when
on_offonbool—type only
levellevelintlevel_range: [min, max] ⊆ 0–100, default [0, 100]min ≤ v ≤ max
color_temptempint (kelvin)temp_range: [min, max], always presentmin ≤ v ≤ max
color_rgbrgbobject {r, g, b[, w]} of ints 0–255color_model: "rgb" | "rgbw", always presentexactly the model's components, each 0–255
source_selectsourcestringsources: [ids…], always presentv ∈ sources
position_selectpositionstringpositions: [ids…], always presentv ∈ positions — where the channel's picture sits (e.g. an inset corner)
pulse_onpulse_on (+ optional pulse_ms)bool (+ int ms)pulse_ms: [min, max], optionalrest on: on → off for pulse_ms → on (restart); off → on. true starts one; false is a no-op
pulse_offpulse_off (+ optional pulse_ms)bool (+ int ms)pulse_ms: [min, max], optionalrest off: on → off; off → on for pulse_ms → off (momentary)
pulsepulsebool—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 likeTypical 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 touch on. "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_id is 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 is 400 validation_failed). Generate it before sending and reuse it on every retry of the same intent.
  • Retrying with the same cmd_id is always safe. A finished command replays its stored answer; a command still running is joined. Never executed twice.
  • Treat 409 board_offline and 504 timeout as retryable with the same cmd_id; treat 422 as 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.

Statuscode / statusWhat happenedWhat to do
200executedThe device applied it; state is the channel's resulting stateDone
200replayed: trueSame cmd_id inside the 24 h window: the stored result, nothing re-executedTreat as the original answer
422bad_channelNo such channel on this deviceRe-discover channels
422bad_capabilityEmpty set, or a field the channel's capabilities do not carryBuild from discovery
422bad_valueA field has the wrong typeFix the payload
422out_of_rangeA value is outside the channel's constraintsClamp to the constraints
422status: rejectedThe 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_idFinal: the device is the authority; fix the command or re-discover
409device_offlineThe device is not connected. Nothing is queuedRetry 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 storedRetry with the same cmd_id: it runs again
409device_blockedBlocked by NovaOwner 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 belowRetry with the same cmd_id: it runs again
504timeoutNo answer within 8 s. The command stays pending: the device's late answer is stored and arrives as a state.changed webhookRetry with the same cmd_id (it is safe), or wait for state.changed
400validation_failedMissing cmd_id / channel / setFix 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
}
  • status is executed (every entry applied), partial (some entries refused by the device: errors maps channel → reason, still 200) or rejected (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 scene and channel is 400 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.

Availability: the relay runs over the device's live link. Until a device has been connected through the Nova link, every command answers 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 seq only grows. Keep the last seq you saw per channel: a state.changed for that channel whose data.seq is 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 channels is empty and seq is null; online is 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 sent

4. 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
CodeMeaning
streaming_unavailable409: the box has no streaming key (no video mixer, or not reported yet)
stale_key409: 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_key400: not a v1 sealed key (or 422 from the box: it does not open)
sealed_key_required400: a partner start always carries a sealed key
bad_url400: url is not rtmp:// or rtmps:// with a host (and no query), at most 300 characters
stream_busy422 from the box: already streaming; stop first
bad_destinations · bad_valid_until400: 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 once

POST/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 nothing

Java 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 by cmd_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}. src says who: app:…, partner:…, kiosk or auto.

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_modeOwner appYour keys
APP_AND_INTEGRATIONScontrolscontrol (the default; every device starts here)
INTEGRATIONS_ONLYsees the box, does not control itcontrol — for a box your system runs, e.g. through bookings
APP_ONLYcontrolsrefused: 409 integration_control_off
  • Read it as control_mode on GET /v1/devices and GET /v1/devices/{device_id}. A change arrives as the webhook device.control_mode {device_id, serial, control_mode, previous}, so your system knows why commands start failing or working again.
  • Covered (refused under APP_ONLY with 409 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 a device.control_mode event 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_INTEGRATIONS for 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_many is a scene the owner applied over Bluetooth. A Wi-Fi action never carries a password. Ignore action types you do not know.
  • at is 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 as state.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)

  1. 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).
  2. Pick the event types you want (the filter can be changed later).
  3. The app shows the signing secret once (Copy / QR). Store it next to your API key; you verify every delivery with it.
  4. Tap Send test event. A real test.ping goes 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

typeWhendata
state.changedA 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.onlineThe device connected to Nova{device_id, serial, seq}
device.offlineThe 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.raisedThe device reported a fault{device_id, serial, seq, event, fault, channel?, …}
fault.clearedThe fault cleared{device_id, serial, seq, event, fault, channel?, …}
device.access_grantedA device entered the account's fleet: your keys can now reach it{device_id, serial, via}
device.access_revokedA device left the fleet (released, transferred away, support action): your keys can no longer reach it{device_id, serial, reason}
test.pingThe owner tapped Send test event{message}
stream.start_requestedSomeone tapped one of your destinations on the box's kiosk: answer with POST …/stream{device_id, serial, destination_id, request_id}
stream.startedThe box is live{device_id, serial, destination_id, space}
stream.stoppedThe stream ended as asked{device_id, serial, destination_id, space, duration_s}
stream.failedThe stream ended by itself{device_id, serial, destination_id, space, reason}
stream.degradedThe uplink is falling behind (the box's send buffer is filling){device_id, serial, destination_id, space, bitrate_kbps, cache_pct}
kiosk.lockedThe 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.unlockedThe kiosks unlocked (the PIN at a kiosk, the app or a partner){device_id, serial, src, change}
kiosk.lockoutFive wrong PINs: the pad is frozen{device_id, serial, retry_s}
device.control_modeThe 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_changeThe 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.

actorMeaning
appThe 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
kioskSomeone at the box's own screen
bleThe owner at the box, over Bluetooth from the app
supportNova support
boxThe box itself: a physical input, a restore after power loss, a correction from a board
Rule: never issue a command in reaction to a 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.

Availability: 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. For state.changed, order per channel by data.seq: drop one whose seq is less than or equal to the last you saw for that channel (your last snapshot's channel seq included). 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 with GET …/state per device.
  • test.ping never 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"
}
StatusUsed for
400Malformed 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
401Key problems: api_key_required, api_key_invalid, api_key_revoked, api_key_expired
403insufficient_scope, partner_suspended, developer_access_revoked
404device_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
409device_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)
422bad_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)
429rate_limited, with Retry-After in seconds
504timeout: 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().

StatusException
400ValidationException
401AuthenticationException
403PermissionDeniedException
404NotFoundException
409DeviceOfflineException (device_offline), StaleKeyException (stale_key), ConflictException (any other 409)
422UnprocessableException (Nova's own checks); a device's status: rejected is a result, not an exception
429RateLimitedException with retryAfter()
504DeviceTimeoutException
noneNovaTransportException: 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

LimitValue
Requests per key120 per minute, fixed window; 429 + Retry-After past it
Command wait8 s for the device's answer, then 504 timeout (the command stays pending)
Your HTTP client timeoutat least 10 s on command calls
Idempotency window24 h per cmd_id, keyed per API key
Webhook timeout10 s for your 2xx
Signature tolerance±5 minutes on the timestamp
Page size25 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

  1. Channel numbers are immutable: safe to persist, safe to key automation on.
  2. Ignore unknown keys in payloads and unknown kind values; the platform grows additively.
  3. Owner presentation (names, icons) is never exposed; maintain your own labels.
  4. Webhooks notify, the API confirms: reconcile with GET …/state, never trust the event stream as the source of truth.
  5. Build requests from discovery, not from product assumptions; the same integration must drive any device, current or future.

Changelog

DateChange
2026-10-25Commands 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-20Contract 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-20Kiosk access (additive): GET …/kiosk, POST …/kiosk/pin and …/kiosk/lock, PUT …/kiosk/visibility, three kiosk.* webhook events.
2026-10-20Streaming (additive): GET/POST …/stream with keys sealed to the box, PUT/DELETE …/stream-destinations, five stream.* webhook events.
2026-10-10v1: 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

RoleCan seeCan do
ADMINEvery page, including Users (staff accounts).Everything: create and manage staff, edit the catalogues, import devices, revoke, unbind, approve developer access.
SUPPORTEvery 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.read permission; 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.

  1. Board role (Boards › New role, ADMIN) — once per kind of board (power, LED, display, master). Pick its check mode while it has no models.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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).
  12. 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)

  1. 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.
  2. 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.
  3. Resend code is available again 30 seconds after the last send (“Resend in 12s” counts down). Resending clears the code box.
  4. “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.
  5. 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

  1. Type your Current password.
  2. 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.”
  3. 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)

  1. Display name (up to 120 characters) and Email.
  2. Role: SUPPORT by default (“Support can do everything except manage staff accounts.”) or ADMIN (“Admins can create, disable and reset other staff accounts.”).
  3. Temporary password, shown in clear so you can pass it on; it must satisfy the password rules.
  4. 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.

  1. Click Edit permissions. A checkbox per scope in the catalogue, each with its description.
  2. Tick what the person needs; with nothing ticked the form warns that they cannot sign in on any station or work in the Inventory.
  3. 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).

TabWhat it showsActions
profileId, 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.
devicesUnits 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)
transfersOffers 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
sessionsEvery 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
notificationsWhat 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
keysThe developer-access state and the owner’s API keys (see below).ADMIN decisions on developer access; platform retirement or emergency revoke of a key.
webhookThe owner’s webhook endpoint and recent deliveries (see below).none — “Registered by the owner in the app; staff cannot edit it.”
logsThe 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:

StateBannerButtons (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)
grantednone, 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

StateMeaningWhat you can do (ADMIN)
DraftThe 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)
PublishedThe channel table is fixed. Config versions say which board models deliver it.Edit name/description · New config version… · Retire…
RetiredNo 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 as defaultState keyed 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 with bad_default on that row. The tick The owner may rename this channel sets editable. Both become the seed’s default and editable and are fixed at publish with the rest of the table.
  7. Two views: Form (rows) and JSON — the same table as text, in the shape of the bench file’s console.channels (model.json, with defaultState and editable), 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).
Limits on a channel are optional narrowing; limits on a port are physical. The console never lets a port deliver a channel it cannot cover, and the backend checks that every narrowing fits inside the port.

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_unit runs 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 with not_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 without inventory.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, named TTB-M1-v1.product_config.json, with its SHA-256 in a header). For one unit, prefer the unit page’s Download Pi seed or nova-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 as certified_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 master block. 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.
  • 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’s master.min_release once 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_set after that; a later report needs a new bench pass); not_pdf for 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.

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.”

  1. 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.
  2. 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.
  3. 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.”
  4. 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).”
  5. 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.
  6. 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.
  7. 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 with multiple_self_roles.
Accepting a board model in any config version freezes that board model’s port profile (see Board model page). Config versions never change channels: a different channel table is a different product model. Any recipe change — boards, parts, harnesses — is a new config version; every unit traces to one version that explains everything inside it. The ERP’s Assemble page offers released versions only.

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

LevelExampleMeans
RolePBWhat a board is for (power, LED, display, master). Code and name never change.
ModelPB/M1The functional contract: ports, capabilities, limits. One per hardware generation.
RevisionPB/M1/RevEA PCB that implements the model. Letters follow the hardware team’s lineage.
ReleasePB/M1/RevE/0.3.0Firmware 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 modeMeaning
HandshakeThe board announces itself to the master with its handshake key at power-on; the master verifies and updates it.
Self-checkThe 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_use if 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 as HDMI-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)

  1. 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.
  2. JSON: the same profile shaped like the model file’s console.profile — { services, surfaces: [{ id, capabilities }], hosts_roles, drives } — so the repo’s MB/<Mn>/model.json profile pastes straight in; switching back parses it and shows a shape error under the box.
  3. Save service profile (sticky bar). A refusal (unknown service, surface capability or drive; profile_frozen once 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)

  1. 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.
  2. 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).
  3. 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 are power, maxA, pwm. A seed’s port facts must match them (seed_port_mismatch).
  4. 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.
  5. 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.

StateMeaningRow verbs
DraftUploaded or registered, not yet available to devices.Publish… · Withdraw… · Factory image / OTA image (uploads only; the links answer only once published)
ReleasedAvailable to devices and to the flashing station. One released version per revision is current.Make current… (when not current) · Withdraw… · Factory image · OTA image
WithdrawnPulled 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.bin from tools/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 with profile_required, a fingerprint that is not the model’s current one with profile_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 .bin itself, 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_stale shown 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:

VersionBuilt withHeld byBadge
1.2.0-dev.NTEST keys: the dev profile and the prod-profile bench rehearsals (--bench-egress / --bench-ssh)the dev consoleDev
1.2.0-qa.Nthe QA profile and QA pins (--trust build/qa/trust.env)the QA consoleQA
1.2.0production keys (no tag), stamped so in its image.json (test_keys false)the production consoleProd
1.0.14no tag, but its image.json says TEST keys (test_keys true): the bench images from before the tagshistory on the dev consoleDev (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.10 is 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.N image, QA never a -dev.N, production only bare versions.
  • Board firmware (PB, LB …) stays bare x.y.z in 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.
StateMeaning
RegisteredPre-registered by the factory; claimable with the label.
ClaimedBound to an owner.
RevokedPlatform block: cannot be claimed and is refused at connect. Reversible with Restore.
Claim policyMeaning
Locked (default)The owner binding survives a reset; only the owner or a cloud-side release changes it.
OpenA 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 statusMeaning
PendingThe code is redeemable; the devices keep working under the sender until it is accepted.
AcceptedEvery device moved to the receiving account in one transaction.
CancelledWithdrawn before redemption (by the sender, replaced by a newer offer, or a device left the sender’s ownership).
ExpiredNot 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

CardShowsADMIN action
UnitIdentity 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)
ERPUnit (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)
ConfigWhat 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}.

BadgeMeansWhat 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.
    FaultRaised byDetailsWhat 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 refusedA 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)Novabox has vN, current is vMThe 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)Novarole: 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.

CardShowsEmpty state
StreamingState (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.
KioskLock (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 (its product_config.json) of a released config version, signed by the cloud; it lands on the Facts queue as config.push and 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_mismatch otherwise), 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 answers config_push_refused and 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 with config_not_released (a draft), no_seed, and seed_version_mismatch when the stored seed names another version (the box would refuse it on every connect). The log records device.config_push each time and device.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 signed ota.offer fact 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 with not_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 records device.ota_offer with 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.secret address, 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.

Claiming itself happens only in the mobile app with the label. The console reads bindings and offers the support-desk Unbind; it never claims a unit for someone.

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)

  1. 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.
  2. 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 (aabbccddeeff or AA-BB-… becomes AA: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.
  3. 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).
  4. 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.

StageConditionSet byMeaning
ReceivedNot verifiedReceive lotBorn with its serial; not flashed yet.
Dead on arrivalFaultystation flash FAIL, no chip idNo response over USB; the chip id could not be read, so nothing is bound.
Flash failedFaultystation flash FAIL with a chip idChip bound to the serial, but the write or the firmware's banner failed. A later good flash clears it.
FlashedNot verifiedstation flash PASSRunning the current release; waiting for its serial step. Flashing never makes a board OK.
SerialisedNot verifiedthe station's serial stepCarries 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 failedFaultyRecord EOL FAILAn output failed under load. A later EOL pass clears it.
EOL passedOKRecord EOL PASSEvery 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 with mirrored); maker and maker part number stay editable (Blackmagic Design, the ATEM's part number). Serials are NVA-PT-…; in stock opens the OK parts of the model. Flipping the board model's Vendor unit attribute re-mirrors the revision, refused with item_model_has_stock once 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; refusals bad_vendor_serial, vendor_serial_taken, vendor_serials_required under 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 role VM”.
  • 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.

  1. 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.
  2. 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).
  3. 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.
  4. 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.”
  5. Vendor-unit roles (ruled 2026-10-23: the ATEM is a part, not a board). A role whose accepted models are all vendor units (VM of 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.
  6. 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).
  7. 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)

  1. The dialog shows Identity activated at the station — the certificate the station's identity job (Install identity…, nova-station cert pi or image --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 answers identity_not_activated). Pick the Claim policy (LOCKED, or OPEN for demo units). Pre-register.
  2. 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)

  1. 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.”
  2. 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.
  3. 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.
  4. Device EOL in RMA (nova-station eol --unit at 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_blocked otherwise: 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 (actor service:<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.
  5. 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 with eol_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.

KindWhereWhat it meansWhat to do
vendor_serials_requiredReceive lot; part pageA 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_serialReceive lot; part pageA vendor serial is not a MAC.AA:BB:CC:DD:EE:FF — six pairs of hex digits from the unit’s label.
vendor_serials_countReceive lotThe number of vendor serials differs from the quantity.One per unit: fix the quantity or the list.
vendor_serial_takenReceive lot; part pageA 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_onlyReceive lotVendor serials were sent for a board or consumable lot.Only part lots carry vendor serials.
mirroredItem model pageName 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_stockBoard model › AttributesThe 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_missingConfig version checklist; AssembleThe 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_unitAssembleA board was named for a vendor-unit role.Pick the received vendor part under Parts instead.
role_mixes_vendor_unitsAssembleA 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_missingAssembleThe picked vendor part has no MAC recorded.Record its vendor serial on the part page first.
role_twiceAssembleTwo 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.

While you are signed in on a station, anyone who can use that computer can flash boards in your name. Keeping the station safe is your responsibility: sign out every time you leave it, even briefly (the station panel's Sign out, or the agent's 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)

  1. 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.
  2. 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.pkg and 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 logout

The 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.

  1. 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, or serve for 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 install nova-station-dev and 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 static 10.42.0.1/24 plus dnsmasq (copyable commands); Windows — Internet Connection Sharing onto the adapter (it becomes 192.168.137.1); the firewall rule Nova Station board EOL (TCP 9000) comes with the setup program, and if Windows still asks about nova-station choose 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.1xx address), 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 \\.\PhysicalDrive2 and asks for PhysicalDrive2); 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 dd without 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:

ScopeAllows
firmware.readRead board revisions, download their released firmware and a unit's released Pi seed
device.testStation: run the device EOL — claim the unit as the factory tester, drive every channel, record the verdict
device.cert.issueStation: issue a unit's identity certificate — the Device CA signs the Pi's CSR at assembly or for a replacement gateway
board.flashFlash boards and record the result
inventory.writeStation: write board and part rows to the Inventory (serial allocation, flash results)
inventory.readConsole: see the Inventory (models, lots, boards, stock)
inventory.receiveConsole: receive lots, author part / consumable models, record part intake
inventory.testRecord board and device end-of-line results: in the console, and a station's board EOL rig
pi.imageStation: write a gateway Pi's card from the current Pi image release of its MB revision
inventory.assembleConsole: assemble, verify, pre-register and ship units; print labels
inventory.rmaConsole: 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.

While you are signed in on a station, anyone who can use that computer can flash boards in your name. Keeping the station safe is your responsibility: sign out every time you leave it, even briefly (the station panel's Sign out, or the agent's 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.

While you are signed in on a station, anyone who can use that computer can flash boards in your name. Keeping the station safe is your responsibility: sign out every time you leave it, even briefly (the station panel's Sign out, or the agent's 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

  1. 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.
  2. It shows when the code expires (codes last 10 minutes).
  3. 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.
  4. 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 saysWhat to do
No sign-in with this codeCheck 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 expiredRun the agent’s login command on the station again for a new code.
Already approved / Already usedNothing: the station is (being) signed in.
DeniedSomeone refused it. Run the agent’s login command again if it was yours.
This station has been revokedIts 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.

TabWhat 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 (+).
ActivityEverything 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.
DeveloperOnly when Nova has granted you developer access and you switched it on under Account. API keys, webhook, the emergency stop.
AccountYour 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_error means 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…”).
Factory-tester accounts (used by Nova’s end-of-line test stations) see exactly the same screens. The only difference: a box claimed by such an account carries a grey factory test badge on its card and on its Details tab. Staff set and remove this mark in the console; the app has no switch for it.

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

  1. Install the Nova app on your phone. There is no Play Store listing yet; Nova gives you the installer.
  2. Open Nova. A spinner shows briefly while the app checks whether you are still signed in.
  3. If you are not signed in, the sign-in screen opens (“Nova · Sign in to your account”). New here? Tap Create account.
  4. If you were signed in before, the app opens straight on Devices.
Scanning a Nova label or transfer QR with the phone’s own camera app opens the Nova app once it is installed (see “Claim and transfer links”). Without the app, the link opens a Nova page with Get the app, which leads here.

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

  1. Fill in Display name, Email, Password and Confirm password. Your display name is how Nova addresses you; nobody else sees it.
  2. 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
  3. Under Confirm password the app says whether the two passwords match.
  4. Tap Create account. It stays greyed out until every field is filled, the email contains @, the checklist is complete and the passwords match.
  5. 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

  1. The screen says “Check your inbox” and names the address the code went to.
  2. Type the code in 6-digit code (digits only; the field stops at six).
  3. Tap Verify.
  4. 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

  1. Type your Email and Password. The eye icon in the password field shows or hides what you typed.
  2. 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

  1. “Reset your password”: check the Email and tap Send code.
  2. “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.
  3. Type a New password (same six-rule checklist as sign-up) and Confirm new password.
  4. 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-tabSee
DevicesThis list.
MomentsMoments across devices
TransfersTransfers 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)

  1. The screen “Scan the label” opens with the camera. The first time, Android asks for camera access: allow it.
  2. 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.
  3. 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

  1. On the scan screen, tap Enter serial & code instead.
  2. Type the Serial and the Claim code (10 characters; dashes and spaces are ignored, letters are made capital for you).
  3. 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

  1. “Device added” shows the model and serial. If the box was already on your account, it says “Already yours” instead.
  2. Optionally type a name in Name this device (optional) (up to 32 characters, shown only to you).
  3. 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.

  1. On the scan (or typed) screen, tap Find the box nearby.
  2. 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.
  3. 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.
  4. 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”).

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

  1. Tap Rename (pencil). Greyed out while the box is blocked.
  2. Type up to 32 characters. Leave it empty to go back to the model name.
  3. 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

TabWhat it holdsHow 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.”
ChannelRead-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.
NameYour 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.
CurlThe 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.

Restart and Unlock (pulse controls) are not in the app yet. Some channels are meant to be pulsed rather than held on or off (a short “Restart” off-pulse, a short “Unlock” on-pulse). The app shows such channels with the ordinary power toggle only; it has no pulse button. When the app gets them, this section will say so.

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).

ChoiceThe appYour integrationsUse it for
App and integrations (default)controlscontrolEvery box starts here.
Integrations onlyshows the box, does not control itcontrolA box your own system runs, e.g. through bookings: nobody undoes a booking from the app by accident.
App onlycontrolsrefusedA 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.

ButtonConfirmationWhat 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

  1. 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.
  2. 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

  1. Tap the scene’s row (not the ▶). It expands into the editor; only one is open at a time.
  2. Name: up to 32 characters.
  3. “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.
  4. Save changes is enabled once something changed. A scene must set at least one channel.
  5. Delete (red) asks “Delete “name”?”. Integrations that apply that scene number get “scene not found” afterwards; the number is never reused.
A transferred box arrives without scenes: the new owner starts fresh.

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

  1. Tap the floating New moment button, or tap an existing row to expand its editor.
  2. Type a Name (up to 32 characters).
  3. “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”.
  4. Save moment / Save changes (at least one scene must be picked). Cancel closes a new one.
  5. 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

  1. Tap Add a channel (or the + in the top bar).
  2. 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.
  3. 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.
  4. 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

  1. 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.
  2. 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).
  3. 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.

Messages you may see: “The box is already streaming. Stop it first”, “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.” (code 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

  1. 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.
  2. Anyone who types the right PIN on either screen unlocks both. Anyone at an unlocked screen can tap Lock (top right).
  3. Lock / Unlock here work from anywhere, without the PIN. Lock stays greyed until a PIN is set.
  4. Five wrong PINs freeze the pad for 5 minutes and you get a notification; Unlock in the app clears it.
  5. 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”.

A box that changes owner or is factory reset starts unlocked, with no PIN and every page shown. Every lock, unlock and PIN change appears in the device's Activity.

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

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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.

  1. The card shows whether the box reaches Nova, its wired network, and whether Wi-Fi is set up.
  2. 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.
  3. 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.
  4. 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

  1. Devices › Transfers › Transfer devices.
  2. 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.
  3. Type Your password. It is asked every time property is handed over.
  4. 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.
Nova shows the code only on this screen. Send it by any channel you trust. If you lose it, cancel the transfer and create a new one. Making a new offer for a box replaces its old offer.

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

  1. 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.
  2. Tap Look up.
  3. Check the preview: “N devices offered”, each with a partly hidden serial and the model, and Code valid until. The sender is not shown.
  4. Tap Accept device (or Accept N devices). Accepting makes them yours and ends the sender’s access. Names and scenes start fresh.
  5. 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

  1. Notifications are off (Android 13 and newer, until you allow them): tap Allow.
  2. 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.
  3. Filter chips: All · Security · Devices · Integrations.
  4. 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

  1. Open the Activity tab. On Android 13 and newer a card says “Notifications are off”.
  2. 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:

CategoryCoversMute in the app?
Property & securityClaims on your devices, transfers, sign-outs, support actions.No.
Availability & faultsA device went offline or raised a fault.Per device: Settings › Offline and fault alerts.
Integration healthWebhook paused, API key about to expire.No.
Offline accessA 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

TipsWhenSteps
WelcomeFirst sign-in, on Devices
  1. Add device — Add your box: scan the code on its label, type it, or find it nearby.
  2. Activity — Everything that happens to your boxes lands here.
  3. Account — Your account, security and Help. Replay these tips from Help.
The device pageFirst device page
  1. Status beside the name — How this box is reached right now: the dot is green when it is online, the Bluetooth mark blue when your phone reaches it over Bluetooth.
  2. Tabs — Details, Channels, Scenes, Activity, Settings.
  3. Channels — Tap the switch for power; tap the row for dials and colour.
  4. Settings — Wi-Fi, notifications and changes of ownership live in Settings, away from the controls.
A channel’s controlsFirst channel row opened
  1. Channel tabs — Control first; Name lets you rename the channel and pick its icon.
  2. Dials — Changes are sent when you let go; the value shown is the box’s answer.
ScenesFirst Scenes tab with a scene
  1. Apply — ▶ applies the scene in one go.
  2. A scene — Tap the row to edit it.
  3. Build a scene — Build one here, or snapshot the current state from Channels.
MomentsFirst Moments tab
  1. Moments — A moment sets a scene on several boxes at once.
  2. New moment — Start one here.
ActivityFirst Activity visit
  1. Needs attention (when shown) — Things still open stay pinned here until they clear.
  2. Filters — Filter by Security, Devices or Integrations.
  3. Notifications are off (when shown) — Turn on notifications here to hear about faults and transfers.
Reached over BluetoothFirst time a page reaches the box over Bluetooth (what’s new 2026-10-21)
  1. Over Bluetooth — The blue Bluetooth mark: your box is not online, so this page reached it over Bluetooth by itself. The controls are the same.
  2. Settings — Things that need Nova say “Needs internet” until the box or phone is back online. The Bluetooth link stays open while you move around the app.
Wi-Fi over BluetoothFirst visit to Settings › Wi-Fi
  1. Wi-Fi — A box’s Wi-Fi can only be changed over Bluetooth, standing next to it.
  2. Looking for the box — If the box is online, short-press its button; the page connects by itself within 10 minutes.
OfflineFirst time the offline line shows (what’s new 2026-10-21)
  1. Offline — No internet: you see what this phone saved. Nothing is lost.
  2. Needs internet — Actions that need Nova wait here. Boxes nearby still work over Bluetooth.
Remote controlWhat’s new 2026-10-21, developer access only, device › Settings
  1. Remote control — Choose who may control this box remotely: your app, your integrations, or both.
  2. Save remote control — Needs internet. Bluetooth at the box is never blocked, and changes made there are reported.
Stream keysFirst Stream tab or Account › Stream channels
  1. Stream keys — Stream keys stay on this phone, locked by your screen lock. Nova never has them; a new phone needs them again.
  2. Go live — Going live asks for your fingerprint or screen lock, then hands the key to this box only.

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.
Nova keeps one line per tip for your account (finished or skipped, and at which step) and nothing else: no tracking of where you tap. The line is deleted with your account.

Change password

Where: Account › Change password. When: any time you know your current password (otherwise use Forgot password).

  1. Type your Current password.
  2. Type a New password (same six-rule checklist) and Confirm new password.
  3. 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 saysWhat it meansWhat you can do
Developer accessNot requested.Request access: a dialog asks “What will you build?” (optional, up to 300 characters); tap Send request.
Developer access requestedWaiting 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 grantedNova declined, with its reason.Request access again once the date shown has passed.
Developer access suspended by NovaNova 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

  1. Tap Create key. Give it a Label and pick Scopes: COMMAND (send commands and scenes) and/or READ (read state). Tap Create.
  2. 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 seeWhyWhat to do
Back on the sign-in screen with a red bannerYou 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_errorThe phone cannot reach Nova.Check the phone’s internet and try again (most screens have a Retry button).
“Connecting to Nova…” does not go awayThe live connection cannot open.Check the internet; leave the app and come back.
Controls greyed out, box offlineThe 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 addingSomeone else owns the box.Ask the seller to release it, or contact support with proof of purchase.
Camera does not open when scanningCamera 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 deviceA 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 outThe 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 notificationsNotifications 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 tabAccess 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.

WhoUsesFor
Operator (factory, bench)nova-station flashFlash boards, read PASS/FAIL
Operator (EOL rig)nova-station eol boardTest one flashed board on the rig over the bus, port by port
Operator (imaging station)nova-station imageWrite a gateway Pi's card or disk with the sealed image, and give the box its identity
Operator (imaging station)nova-station eol piTest one imaged master board (Pi): the box checks itself over its board LAN
Engineertools/release.pyBuild one firmware version and its record
Engineer / admintools/catalogue_sync.py or the revision pagePut the board records and the release into this console, publish it, make it current

Where it sits in the flow

  1. Hardware tests pass on a new board revision; its records exist in the repo.
  2. The engineer builds a release (Building and publishing a release).
  3. The release is uploaded here, published and made current (Console application › Board revision page).
  4. Every board of that revision is flashed at a station (Flashing a board).
  5. Each flashed board is tested on the EOL rig (Board EOL on the rig).
  6. A gateway Pi gets its card or disk written with the sealed image at a station (Writing a gateway Pi).
  7. 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 on keeps 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. New nova-station map install --unit … --rma … (a board-only RMA swap; the console's Install device map…) and map show. eol --unit says a refused start (eol_blocked) in words. The manual cert issue is 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 flash and Pi images for image — 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 image pi.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, so seed, image --unit and image --seed check that it matches the seed sent beside it (seed_hash_mismatch otherwise, 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 seed writes the seed and its cloud signature (product_config.sig), and image --unit stages both; image --serial refuses a sticker of another MB model before writing and, after the boot, writes the board's identity file and marks it SERIALISED; flash --efuse burn marks a burned board SERIALISED; eol board also 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, targets and cert pi (built, not yet proven on the bench; macOS and Linux only, run from a checkout of the firmware repo for now).
Both paths run the same job and write the same records: the terminal (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.

FlavourProgramSettings folderLocal portConsole
devnova-station-dev~/.nova/station-dev/8765set by hand: config set console_url / console_origin
QAnova-station-qa~/.nova/station-qa/8766built in: https://api.qa.iot.novacube.io, origin https://qa.iot.novacube.io
productionnova-station~/.nova/station/8767built 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 raw door operations, cloud locate, efuse off|virtual|burn, eol … --offline, the bench flags) and config set of every setting. NOVA_STATION_HOME still 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, outbox and config (config set takes only station_key and the station name). No engineering options, no bench flags.
  • nova-station (production) — login, whoami, doctor, serve, autostart and config (station_key and the station name). Every job starts from the console's dialogs (the local service). There is no logout: 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_KEY and NOVA_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

  • /health carries flavour, program and port.
  • Autostart registers io.novacube.nova-station-dev, io.novacube.nova-station-qa or io.novacube.nova-station (macOS); on Windows the logon task NovaStationDev, NovaStationQA or NovaStation.
  • Installer names: <program>-<version>-macos-arm64.pkg (package identifier io.novacube.nova-station-dev, -qa or io.novacube.nova-station, so the three coexist) and <program>-<version>-windows-x86_64-setup.exe.
  • images.csv gains a version column.
After updating to 1.0.0, every imaging station runs the writer helper install once more (helper version 2: the .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.
  • doctor prints the flavour, the console it talks to (pinned or set), the settings folder and the port.
This book writes the commands as 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).

NeedHow
The agentNo 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 cableCharge-only cables are the first suspect when the board's port never appears.
Network access to the IoT core backendPlus this machine's station key: an admin creates the station in Console › Stations and copies the key, which is shown once.
24 V bench supplyOnly if loads are checked at the station. Flashing itself runs on USB.

macOS: the installer package

  1. Download <program>-<version>-macos-arm64.pkg (nova-station-dev, nova-station-qa or nova-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.
  2. 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-write and /etc/sudoers.d/nova-station with its one rule for you — exactly what install-helper.sh does (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.
  3. Then, in a terminal, the first-run commands below: the station key (dev: also console_url and console_origin), <program> login, <program> autostart on.
  4. Check: <program> doctor, and <program> targets whose first line is writer helper: ready.
  5. Uninstall: <program> autostart off, then sudo rm /usr/local/bin/<program> and sudo pkgutil --forget io.novacube.<program>; the helper goes with sudo sh install-helper.sh --uninstall from a checkout, or sudo rm /usr/local/libexec/nova-station-write /etc/sudoers.d/nova-station once no flavour is left. Your settings folder (~/.nova/station…/) stays.

Windows: the setup program

  1. Download nova-station-<version>-windows-x86_64-setup.exe and run it; accept the administrator prompt (UAC). It is not signed yet: if SmartScreen stops it, choose More info › Run anyway.
  2. 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.exe or C:\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 service NovaStationWriter for Pi imaging (runs as the system; only the Windows user who ran the setup may talk to it), the firewall rule Nova Station board EOL (TCP 9000) (inbound TCP 9000 from 192.168.137.0/24, the rig's Internet Connection Sharing network, for nova-station.exe only) and the CH343 driver when bundled.
  3. 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 … and nova-station autostart on as you. Left empty, run the first-run commands below in a normal PowerShell (not as administrator).
  4. Check: nova-station doctor, and nova-station targets whose first line is writer 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 doctor and the console's Flash dialog say when a newer build is published for your machine. Check your version with nova-station --version.
  • macOS: download the newer .pkg from 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 on starts 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 run pipx reinstall nova-station. pipx upgrade does not work for this kind of install.
  • Your settings, station key, sign-in and downloaded releases are kept across updates. Run nova-station doctor afterwards.

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 8766

A 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 on

doctor 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, whoami and doctor print what you may do.

Sign in and sign out

  1. Run nova-station login. It prints a link and a code like 68H3-MZJW.
  2. 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.
  3. The terminal says "Signed in as …". nova-station whoami shows it again later.
  4. 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.
  5. The backend decides when a session ends, not the agent. From agent 0.6.6 every /health asks 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.
  6. When you leave the station, sign out: logout on a dev or QA station (nova-station-dev logout, nova-station-qa logout); the production nova-station has no logout command — 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.
Keeping the station safe is your responsibility. While you are signed in, anyone who can use that computer can flash boards in your name, and the console records those flashes as yours. Run 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….

macOS: the .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:

  1. 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 (source station/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.sh

It 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: ready

The 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.

  1. 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, as disk4 on 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.test to C:\Windows\System32\drivers\etc\hosts (the macOS step above says why; nova-station doctor checks 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

  1. Connect the board with a USB data cable. Run nova-station ports if you are unsure which port it is.
  2. Run nova-station flash --revision PB/M1/RevE. Use the revision code exactly as the console shows it.
  3. Follow the prompts. Type the answer shown in brackets, for example continue.
  4. 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.
  5. 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. --serial gives it up front.
  6. 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 with board.flash + inventory.write. How: nova-station config set efuse burn (or --efuse burn per 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 stay efuse off (the default) and remain FLASHED; virtual rehearses the step on a file without touching the board.
  7. 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).
Every flash is recorded under the person signed in on the station. Do not leave a station signed in: 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.

PromptWhenDo
Hold BOOT, tap RESET, release BOOT, then ContinuePower boards (no auto-reset circuit), or when the agent could not connectExactly that, then continue or retry
Press RESET on the board, then ContinueAfter writing a board the agent cannot resetPress RESET, then continue. The agent then asks the board which firmware it runs
The board did not answer within N sThe board did not report its firmwareClose any serial monitor, press RESET or reseat the cable, retry once
Cannot open portWrong cable, or a serial monitor holds the portClose the monitor, retry
Scan the board's sticker (or type its serial)Before the write, unless --serial was givenScan 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 allskip (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

FlagMeaning
--port <device>Pick the port. Without it the agent uses the single USB board it finds.
--version x.y.zSafety check: must equal the console's current version.
--dry-runDownload and cache only. Nothing is written to the board.
--offlineBackend unreachable: use the newest cached copy, with a warning. Bench use only.
--release-file <file> --version x.y.zEngineering only: flash a local factory image without asking the console.
--notes "…"Free text stored with the result.
--serial NVA-PB-00007-4The 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 session

serve 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.

  1. 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.
  2. 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.
  3. 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.
  4. PASS or FAIL with the reason; the board row shows the chip id, firmware and condition at once.
The dialog's Sign out link ends your agent session; signing out of the console alone does not. The 30-minute idle sign-out is the safety net.

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

ItemWhat
Station machineAny machine with the agent (0.4.0 or later), signed in.
Rig network portA dedicated Ethernet port (a USB-Ethernet adapter) cabled straight to the board, never the office network.
Bench supply24 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 harnessOne plug-in harness per board model, with a known load on every output.
MeterOptional; needed with --measure.
Sticker scannerThe 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:

SystemSet-upBoard dials
macOSSystem 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
LinuxNetworkManager'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
WindowsInternet 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.1

One 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         # Windows
Turn Internet Sharing off again and re-cable before imaging or testing a box. There the box's Pi is the gateway on its own board LAN (10.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 console

What it does

  1. Looks the sticker up in the Inventory: the board must be at stage FLASHED, SERIALISED or EOL_*, and not in a device.
  2. Listens on 0.0.0.0:9000 and waits for the board's announce. Power-cycle the board after starting the run.
  3. 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.
  4. 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, typing redo at 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.
  5. 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 holds

What happens, in order

  1. 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.
  2. 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-file for engineering (--sha256 refuses a different file). .img, .img.xz, .img.gz (and .zst on Python 3.14 or later) are written decompressed.
  3. 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 disk4 types it up front.
  4. 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: the NovaStationWriter service 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).
  5. 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-file is staged from a checkout of the firmware repo (--provision-src <checkout>/master_boards_Raspi or nova-station config set provision_src …); --no-provision is only for an --image-file that is itself a sealed image, and --no-provision, --provision-src and --seed without an --image-file to 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's product_config.json and product_config.sig beside it (the cloud's signature, checked as nova-station seed does; --seed FILE needs FILE's .sig beside it and is refused when the two do not belong together), set to run once at first boot.
  6. 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 a 10.42.0.1xx address) and power on. The agent reaches the box at kiosk@10.42.0.1 over ssh and waits for its first boot to finish (a checkout-provisioned card: each provisioning step; it takes tens of minutes). --boot-timeout sets the wait in minutes.
  7. Boot self-test. The box's services are enabled, /data is mounted, eth0 is 10.42.0.1/24, the box software and pinned roots are present; the Pi's own serial number is read.
  8. Board row (--serial). The MB sticker is reported to the Inventory like a flash (chip id pi:<Pi serial>, the image release, PASS) → stage FLASHED. A refusal fails the job (the local row records it): fix the cause, image again.
  9. 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).
  10. 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.
  11. 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.

Today the station reaches the box over ssh on the board LAN, because the current image has an ssh server (bench grade). The sealed image will have none; the same steps then move to a station endpoint on the board LAN.

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

  1. Looks the sticker up in the Inventory: an MB revision at stage SERIALISED or EOL_*, not in a device, bound to a Pi.
  2. Fetches the MB model's service profile and its fingerprint from the console.
  3. Reaches the box, which must be the Pi the row is bound to: another box is not tested and nothing is reported.
  4. 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.
  5. Reports the checks to the board row: the backend derives PASS → EOL passed (condition OK) or FAIL → EOL failed (faulty, failed end-of-line).
CheckPASS when
identity fileThe box's identity file carries this serial, the chip pi:<SoC serial> and the row's model.
boot mediumThe 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 slotThe 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.
dnsmasqActive (the board LAN's DHCP).
BLEModels with BLE only: the controller is powered.
root filesystemAgent 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).
RTCThe real-time clock is present and readable (the backup cell is ruled but not fitted on the bench Pis).
cloudAgent 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 syncAgent 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.
imageThe image's model, revision, model profile fingerprint, services and proto match the console's model.
image releaseThe box runs the release the row was imaged with.
supervisorThe supervisor is active.
service ble / kiosk / videoThe model's units are installed and enabled (they start once a seed lists them; a box before assembly has none).
no foreign servicesNo unit of a service the model does not promise is installed.
output HDMI-A-1 / DSI-2Each 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.

Today the station reaches the box over ssh, which only the bench image has. The sealed production image has no ssh server; a production-image EOL needs the station endpoint on the board LAN.

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.

No password prompt in the service. The service cannot ask for the administrator password, so writing a disk from the console needs the writer helper and, on macOS, its sudoers rule (the .pkg installs both; Windows: just the running service; 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 happenedWhat the station doesWhat the console shows
The tab or window was closed while a question waitedThe 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-runNothing 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-runNothing: 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 reportedA 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-runThe 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 outThe 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 tabsEach 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.csv say CANCELLED with 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.csv and 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.csv says 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 runs

The 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.1

It leaves three files beside the release record in …/RevE/releases/0.3.1/:

FilePurpose
PB-M1-RevE-0.3.1.factory.binThe 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.binThe 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.zipDebug 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-current

plan 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.

CommandWhat it doesOptionsSign-in · scope
--versionPrints the agent version.—no
doctorChecks 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 locateDev 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
loginSigns you in: prints a link and a code, waits for your approval in the console, then shows what you may do.Ctrl-C cancelsthe console refuses without an allowed scope
whoamiWho is signed in, on which station, until when, and what they may do.—yes
logoutDev 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.——
portsLists serial ports, to find the board.—no
revisionsLists board revisions and the current release of each.—yes · firmware.read
flashThe 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
checkRead-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
eolThe 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) · --notesyes · device.test
eol boardThe 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) · --notesyes (not with --offline) · inventory.write + inventory.test
eol piThe 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) · --notesyes · inventory.write + firmware.read + inventory.test
seedDownloads 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 trustWrites 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) · --forceyes · firmware.read
cert piThe 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 installA 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 · --portyes · device.cert.issue
map showPrints 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 · --portyes · device.cert.issue
targetsLists 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
imagePi 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) · --notesyes, 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 lsLists releases kept on this machine.—no
cache pruneDeletes cached releases that are no longer published.—yes · firmware.read
config showPrints 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 valueno
outboxThe 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 / statuson: 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 · statusno
serveStarts 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

PathWhat
~/.nova/station/agent.jsonSettings, 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.csvOne 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.jsonOptional 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.csvOne row per Pi imaging run: time, station, operator, revision, release, checksums, target, bytes, serial, unit, Pi serial, fingerprint, result, seconds, notes
~/.nova/station/pi_ed25519The 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

MessageMeaning and fix
nobody is signed in on this stationRun nova-station login and approve the code in the console.
your station session has endedIt expired, you signed out, or an admin signed you out. Sign in again.
station_key_invalidThe 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_unreachableWrong backend address or no network. Run doctor.
no_current_releaseNothing is published and current for that revision yet.
revision_not_foundThe console has no such revision. Sync the catalogue first.
download sha256 … != catalogueThe stored file does not match its record. Stop and tell engineering.
wrong image is runningThe write did not take. Repeat the BOOT steps.
macOS asks to let Python find devices on the local networkDeny when the backend is on this machine. Allow when it is elsewhere on the network.
-dirtyThe 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 0755The 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 falseThe 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/writeNot 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 stationOne 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 firstThe 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_eolThe 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 answerCable 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 BLEOn 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 releaseThe box runs another release than the row was imaged with (an update, another card): re-image with --serial.
eol pi: FAIL output DSI-2An 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) bootThe 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 answeredCable 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_readymap_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.