| Time | Topic | Source | Payload |
|---|---|---|---|
| No events yet | |||
| {formatTime(ev.received_at)} | {ev.topic} | {ev.source_type} | |
| Name | Type | Streams | Status |
|---|---|---|---|
| No cameras configured | |||
| {cam.name} | {cam.type.toUpperCase()} | {String(props.streamCounts.get(cam.id) ?? 0)} | {cam.enabled ? Enabled : Disabled } |
Connect to an ONVIF camera or NVR by host and credentials. Profiles from the same video source are imported as streams on one camera.
Video sources reported by {props.host}. Each source imports as one camera with its profiles saved as streams.
| Profile | Encoding | Resolution | FPS | Stream URI | |
|---|---|---|---|---|---|
| No profiles returned | |||||
| {p.profile_name} | {p.encoding ? {p.encoding} : "—"} | {p.width && p.height ? `${String(p.width)}x${String(p.height)}` : "—"} | {p.framerate != null ? String(p.framerate) : "—"} | {p.stream_uri} | |
Video sources reported by {props.host}. Each source imports as one camera with its profiles saved as streams.
{props.cameras.length === 0 ? (Entities are reusable content blocks (a camera reference, an HTML snippet, or a web page). Bind one entity to any number of layout cells — edit the entity once and every cell updates.
| Name | Type | Detail |
|---|---|---|
| No entities yet | ||
| {e.name} | {entityBadge(e.type)} | {entityDetail(e)} |
| Name | Hardware | Last Seen | Status |
|---|---|---|---|
| No kiosks paired | |||
| {k.name} | {k.hardware_model ?? "—"} | {k.last_seen_at ? formatTime(k.last_seen_at) : "Never"} | {k.enabled ? Active : Disabled } |
{pc.code}
{formatTime(pc.expires_at)}
Enabled {" "}TOTP is active on this account.
Protect your account with a TOTP authenticator app.
Scan this with your authenticator app (Google Authenticator, Authy, etc.).
{props.secret}
Save these codes somewhere safe. They will not be shown again.
{props.description}
| Name | Details |
|---|---|
| None configured yet | |
| {item.name} {item.badge && ( {item.badge} )} | {item.detail ?? ""} |
No labels attached
)}| Role | Name | URI |
|---|---|---|
| {s.role} | {s.name} | {s.rtsp_uri} |
No streams configured
)}| Name | Resolution | Index |
|---|---|---|
| {d.name} | {String(d.width_px)}x{String(d.height_px)} | {String(d.index)} |
No displays associated with this kiosk
)}
Each input binding fires an event with the configured topic when the
pin's edge triggers. Pi 5's main GPIO chip is gpiochip4;
older Pis use gpiochip0.
| Chip | Pin | Dir | Pull | Edge | Topic | |
|---|---|---|---|---|---|---|
| {g.chip} | {String(g.pin)} | {g.direction} | {g.pull ?? "—"} | {g.edge ?? "—"} | {g.topic} |
No GPIO bindings configured
)}No labels attached
)}| Name | Color | Actions |
|---|---|---|
| No labels | ||
| {l.name} | {l.color ? {l.color} : "—"} | |
Layouts are standalone — they define a grid of regions and bind cameras or other content into them. Attach a layout to one or more displays from the display's edit page.
| Name | Displays | Priority |
|---|---|---|
| No layouts created yet | ||
| {l.name} | {count === 0 ? unattached : {String(count)} display{count !== 1 ? "s" : ""}} | {l.priority === "hot" ? hot : l.priority === "cold" ? cold : normal } |
Create an empty layout. You'll add cells visually on the next page, then attach the layout to one or more displays.
Hover a side + to add a neighbour or expand the cell. Expanding pushes cells in that direction out of the way. Click a cell to edit content in-place.
Pick which layouts this display can show. The kiosk receives only attached layouts in its bundle.
{props.attachedLayouts.length === 0 ? (No layouts attached yet.
) : (| Name | Priority | Default | |
|---|---|---|---|
| {l.name} | {l.priority} | {d.default_layout_id === l.id ? Yes : ""} |
{props.attachedLayouts.length === 0 ? No layouts exist yet. Create one. : "All existing layouts are already attached."}
)}Physical HDMI displays. Created automatically when kiosks are paired.
| Name | Details |
|---|---|
| None configured yet | |
| {d.name} | {String(d.width_px)}x{String(d.height_px)} — index {String(d.index)} |
Auto-refresh every 30 seconds. Online = last seen within 5 minutes.
| Kiosk | Status | Last Seen | CPU Temp | Fan | Bundle | Displays |
|---|---|---|---|---|---|---|
| No kiosks paired | ||||||
| {k.name} | {row.online ? Online : Offline} | {k.last_seen_at ? formatTime(k.last_seen_at) : "Never"} | {tempBadge(k.cpu_temp_c)} | {k.fan_rpm != null ? `${String(k.fan_rpm)} RPM` : "—"} {k.fan_pwm != null && ( ({String(k.fan_pwm)}/255) )} | {row.bundleMismatch ? ( mismatch ) : k.last_bundle_version ? ( {k.last_bundle_version.slice(0, 8)} ) : ( — )} |
{row.displays.length === 0 ? (
none
) : (
row.displays.map((d) => (
{d.name}: {String(d.width_px)}×{String(d.height_px)}
))
)}
|