Skip to main content

HxTP/3.1 Dashboard Architecture Guide

For Frontend Developers Building the Cloud Dashboard

This guide explains the architecture, state management, and integration patterns for the HxTP/3.1 cloud dashboard.


Table of Contents​

  1. Overview
  2. Device Discovery
  3. Device State Management
  4. Claim States
  5. Active Lifecycle
  6. Provisioning Status
  7. Trust Tier Display
  8. Descriptor Display
  9. Session Expiration
  10. Real-Time Updates

Overview​

The dashboard manages device lifecycle through HxTP/3.1 protocol states:

User Dashboard
↓
[Frontend App - React/Vue/Svelte]
↓
REST API Layer (/api/v1/devices/*)
↓
Backend Service (Node.js)
↓
MQTT Broker (bidirectional device comms)
↓
Devices (ESP32/ESP8266 with Micro SDK)

Key Concepts​

  • Device: Physical IoT device with Ed25519 identity
  • Device ID: UUIDv5 derived from public key (deterministic)
  • State: Current lifecycle position (PENDING_CLAIM → CLAIMED → ACTIVE)
  • Session Token: MQTT authentication credential (ephemeral)
  • Trust Tier: Security level based on identity verification
  • Descriptor: Device capabilities and command interface

Device Discovery​

List All Devices​

Endpoint: GET /api/v1/devices

// Frontend
async function loadDevices() {
const response = await fetch("/api/v1/devices", {
headers: {
Authorization: `Bearer ${authToken}`,
},
});

const devices = await response.json();
return devices;
}

// Expected Response
[
{
device_id: "550e8400-e29b-41d4-a716-446655440000",
name: "Living Room Relay",
device_type: "relay",
state: "ACTIVE",
created_at: "2026-05-11T10:00:00Z",
last_seen: "2026-05-11T10:45:32Z",
public_key: "a1b2c3d4...",
trust_tier: "verified",
},
{
device_id: "550e8400-e29b-41d4-a716-446655440001",
name: "Bedroom Thermostat",
device_type: "thermostat",
state: "PENDING_CLAIM",
created_at: "2026-05-11T10:30:00Z",
last_seen: null,
public_key: null,
trust_tier: "unverified",
},
];

Filter by State​

async function getDevicesByState(state: string) {
const devices = await loadDevices();
return devices.filter((d) => d.state === state);
}

// Usage
const activeDevices = await getDevicesByState("ACTIVE");
const claimingDevices = await getDevicesByState("PENDING_CLAIM");

Device State Management​

State Machine​

// utils/deviceStates.ts
export const DEVICE_STATES = {
IDLE: "IDLE", // Device not yet registered
PROVISIONING: "PROVISIONING", // WiFi config in progress
PENDING_CLAIM: "PENDING_CLAIM", // Awaiting claim completion
CLAIMED: "CLAIMED", // Identity verified
MQTT_LINKING: "MQTT_LINKING", // Connecting to MQTT
HELLO_SENT: "HELLO_SENT", // Sent HELLO, waiting for ACK
ACTIVE: "ACTIVE", // Fully operational
RECONNECTING: "RECONNECTING", // Lost connection, retrying
REVOKED: "REVOKED", // Permanently blocked
ERROR_STATE: "ERROR_STATE", // Fatal error
};

export const STATE_TRANSITIONS = {
IDLE: ["PROVISIONING"],
PROVISIONING: ["PENDING_CLAIM"],
PENDING_CLAIM: ["CLAIMED", "IDLE"], // reset if claim fails
CLAIMED: ["MQTT_LINKING", "PENDING_CLAIM"],
MQTT_LINKING: ["HELLO_SENT"],
HELLO_SENT: ["ACTIVE", "CLAIMED"],
ACTIVE: ["RECONNECTING", "REVOKED"],
RECONNECTING: ["ACTIVE", "REVOKED"],
REVOKED: [], // terminal state
ERROR_STATE: ["IDLE"], // recovery via reset
};

export function canTransition(from: string, to: string): boolean {
return STATE_TRANSITIONS[from]?.includes(to) ?? false;
}

Component: State Indicator​

// components/DeviceStateIcon.tsx
export function DeviceStateIcon({ state }: { state: string }) {
const iconMap = {
IDLE: <CircleIcon />,
PROVISIONING: <WifiSearchingIcon />,
PENDING_CLAIM: <LockOpenIcon />,
CLAIMED: <LockClosedIcon />,
MQTT_LINKING: <CloudSyncIcon />,
HELLO_SENT: <SendIcon />,
ACTIVE: <CheckCircleIcon />,
RECONNECTING: <RefreshIcon />,
REVOKED: <XCircleIcon />,
ERROR_STATE: <AlertTriangleIcon />,
};

const colorMap = {
IDLE: "gray",
PROVISIONING: "yellow",
PENDING_CLAIM: "orange",
CLAIMED: "blue",
MQTT_LINKING: "purple",
HELLO_SENT: "cyan",
ACTIVE: "green",
RECONNECTING: "yellow",
REVOKED: "red",
ERROR_STATE: "red",
};

return (
<div style={{ color: colorMap[state] }}>
{iconMap[state]}
<span>{state}</span>
</div>
);
}

Claim States​

Claim Flow UI​

// components/ClaimFlow.tsx
export function ClaimFlow({ device }: { device: Device }) {
if (device.state !== "PENDING_CLAIM") {
return null; // Not in claim flow
}

return (
<div className="claim-flow">
<h3>Claim Your Device</h3>

<div className="steps">
<Step
number={1}
title="Generate QR Code"
completed={device.claim_session !== null}
>
<QRCode value={device.claim_session} size={200} />
</Step>

<Step number={2} title="Device Scans QR">
<p>Waiting for device to scan and claim...</p>
<ClaimProgressIndicator deviceId={device.device_id} />
</Step>

<Step number={3} title="Verification">
<p>Backend verifies Ed25519 signature...</p>
</Step>
</div>

<div className="claim-status">
<p>Claim expires in: {getClaimTimeRemaining(device)}</p>
<button onClick={() => regenerateClaimSession(device.device_id)}>
Generate New QR
</button>
</div>
</div>
);
}

function ClaimProgressIndicator({ deviceId }: { deviceId: string }) {
const [progress, setProgress] = useState(0);

useEffect(() => {
const timer = setInterval(async () => {
const device = await fetch(`/api/v1/devices/${deviceId}`).then((r) =>
r.json()
);

if (device.state === "CLAIMED") {
setProgress(100);
clearInterval(timer);
} else if (device.state === "PENDING_CLAIM") {
setProgress(50);
}
}, 2000);

return () => clearInterval(timer);
}, [deviceId]);

return <ProgressBar value={progress} />;
}

Active Lifecycle​

Activation Flow​

// hooks/useDeviceActivation.ts
export function useDeviceActivation(deviceId: string) {
const [device, setDevice] = useState(null);
const [isWaitingForHELLO, setIsWaitingForHELLO] = useState(false);

async function waitForActivation(timeoutMs = 30000) {
setIsWaitingForHELLO(true);

const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const res = await fetch(`/api/v1/devices/${deviceId}`);
const updated = await res.json();

setDevice(updated);

if (updated.state === "ACTIVE") {
setIsWaitingForHELLO(false);
return updated;
}

await new Promise((resolve) => setTimeout(resolve, 1000));
}

throw new Error("Device activation timeout");
}

return { device, isWaitingForHELLO, waitForActivation };
}

// Usage in component
function DeviceActivationWizard() {
const { device, isWaitingForHELLO, waitForActivation } =
useDeviceActivation(deviceId);

async function handleActivate() {
try {
await waitForActivation();
showSuccess("Device is now ACTIVE!");
} catch (err) {
showError("Activation failed: " + err.message);
}
}

return (
<div>
<button onClick={handleActivate} disabled={isWaitingForHELLO}>
{isWaitingForHELLO ? "Waiting..." : "Check Status"}
</button>
</div>
);
}

Heartbeat Monitoring​

// services/heartbeatMonitor.ts
export class HeartbeatMonitor {
private timers = new Map<string, NodeJS.Timeout>();

startMonitoring(deviceId: string, options = {}) {
const { intervalMs = 60000, timeoutMs = 120000 } = options;

const timer = setInterval(async () => {
const device = await fetchDevice(deviceId);

const timeSinceLastSeen = Date.now() - new Date(device.last_seen);

if (timeSinceLastSeen > timeoutMs) {
this.handleHeartbeatMissed(deviceId, device);
}
}, intervalMs);

this.timers.set(deviceId, timer);
}

private handleHeartbeatMissed(deviceId: string, device: Device) {
if (device.state === "ACTIVE") {
// Transition to RECONNECTING
updateDevice(deviceId, { state: "RECONNECTING" });

// Notify dashboard
emit("device-heartbeat-missed", { deviceId, device });
}
}

stopMonitoring(deviceId: string) {
const timer = this.timers.get(deviceId);
if (timer) {
clearInterval(timer);
this.timers.delete(deviceId);
}
}
}

Provisioning Status​

Display Provisioning Progress​

// components/ProvisioningProgress.tsx
export function ProvisioningProgress({ device }: { device: Device }) {
const steps = [
{
key: "wifi",
label: "WiFi Connection",
active: ["PROVISIONING", "WIFI_CONNECTING"].includes(device.state),
},
{
key: "bootstrap",
label: "Bootstrap Session",
active: device.state === "BOOTSTRAPPING",
},
{
key: "mqtt",
label: "MQTT Linking",
active: device.state === "MQTT_LINKING",
},
{
key: "hello",
label: "HELLO Handshake",
active: device.state === "HELLO_SENT",
},
{
key: "active",
label: "Ready",
active: device.state === "ACTIVE",
},
];

return (
<div className="provisioning-progress">
{steps.map((step) => (
<ProvisioningStep
key={step.key}
label={step.label}
active={step.active}
/>
))}
</div>
);
}

interface ProvisioningStepProps {
label: string;
active: boolean;
}

function ProvisioningStep({ label, active }: ProvisioningStepProps) {
return (
<div className={`step ${active ? "active" : ""}`}>
{active ? <Spinner size="sm" /> : <CheckIcon />}
<span>{label}</span>
</div>
);
}

Trust Tier Display​

Trust Levels​

// types/trust.ts
export enum TrustTier {
UNVERIFIED = "unverified", // device_id not claimed yet
CLAIMED = "claimed", // Ed25519 identity verified
ACTIVE = "active", // HELLO verified, session token valid
REVOKED = "revoked", // permanently blocked
}

// Display component
function TrustTierBadge({ tier }: { tier: TrustTier }) {
const badgeConfig = {
[TrustTier.UNVERIFIED]: { color: "gray", label: "Unverified" },
[TrustTier.CLAIMED]: { color: "blue", label: "Claimed" },
[TrustTier.ACTIVE]: { color: "green", label: "Verified Active" },
[TrustTier.REVOKED]: { color: "red", label: "Revoked" },
};

const config = badgeConfig[tier];
return <Badge color={config.color}>{config.label}</Badge>;
}

Descriptor Display​

Device Capabilities​

// components/DeviceCapabilities.tsx
export function DeviceCapabilities({ deviceId }: { deviceId: string }) {
const [descriptor, setDescriptor] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function load() {
const res = await fetch(
`/api/v1/devices/${deviceId}/capabilities`
);
const desc = await res.json();
setDescriptor(desc);
setLoading(false);
}

load();
}, [deviceId]);

if (loading) return <Spinner />;
if (!descriptor) return <p>No capabilities</p>;

return (
<div className="capabilities">
<h3>Capabilities</h3>
{descriptor.capabilities.map((cap: any) => (
<CapabilityCard key={cap.id} capability={cap} />
))}
</div>
);
}

interface CapabilityCardProps {
capability: {
id: number;
action: string;
params: Record<string, any>;
};
}

function CapabilityCard({ capability }: CapabilityCardProps) {
return (
<Card>
<h4>{capability.action}</h4>
<p>ID: {capability.id}</p>
<ParametersList params={capability.params} />
</Card>
);
}

Session Expiration​

Monitor Session TTL​

// hooks/useSessionTTL.ts
export function useSessionTTL(device: Device) {
const [ttl, setTtl] = useState(0);

useEffect(() => {
if (!device.session_token_expires) return;

const timer = setInterval(() => {
const remaining = new Date(device.session_token_expires).getTime() - Date.now();
setTtl(Math.max(0, remaining));

if (remaining <= 0) {
clearInterval(timer);
handleSessionExpired(device.device_id);
}
}, 1000);

return () => clearInterval(timer);
}, [device.session_token_expires, device.device_id]);

return ttl;
}

// Display TTL
function SessionTTLDisplay({ device }: { device: Device }) {
const ttl = useSessionTTL(device);
const minutes = Math.floor(ttl / 60000);
const seconds = Math.floor((ttl % 60000) / 1000);

return (
<p>
Session expires in: {minutes}m {seconds}s
</p>
);
}

Real-Time Updates​

WebSocket Integration​

// hooks/useDeviceUpdates.ts
export function useDeviceUpdates(deviceId: string) {
const [device, setDevice] = useState(null);

useEffect(() => {
const ws = new WebSocket(
`wss://api.hestia.local/ws/devices/${deviceId}`
);

ws.onmessage = (event) => {
const update = JSON.parse(event.data);
setDevice((prev) => ({ ...prev, ...update }));
};

ws.onerror = (err) => {
console.error("WebSocket error:", err);
// Fall back to polling
};

return () => ws.close();
}, [deviceId]);

return device;
}

// Usage in component
function LiveDeviceMonitor({ deviceId }: { deviceId: string }) {
const device = useDeviceUpdates(deviceId);

return (
<div>
<p>State: {device?.state}</p>
<p>Last seen: {device?.last_seen}</p>
<TrustTierBadge tier={device?.trust_tier} />
</div>
);
}

Error States​

Handle Device Errors​

// utils/deviceErrors.ts
export const DEVICE_ERROR_MESSAGES = {
CLAIM_TIMEOUT: "Device claim session expired. Create a new device.",
MQTT_CONNECT_FAILED: "Device failed to connect to MQTT broker.",
HELLO_TIMEOUT: "Device did not send HELLO within timeout.",
SIGNATURE_INVALID: "Device signature verification failed.",
SESSION_EXPIRED: "Device session token expired. Device will attempt to reconnect.",
DEVICE_REVOKED: "Device has been revoked and cannot communicate.",
};

// Error handling
async function handleDeviceError(deviceId: string, error: string) {
const message = DEVICE_ERROR_MESSAGES[error] || error;

showErrorNotification(message);

// Log to backend
await logDeviceError(deviceId, error);

// Trigger recovery if possible
if (error === "SESSION_EXPIRED") {
triggerDeviceReconnection(deviceId);
}
}

API Reference​

Get Device Details​

GET /api/v1/devices/{deviceId}

Update Device​

PATCH /api/v1/devices/{deviceId}

Send Command​

POST /api/v1/devices/{deviceId}/command

Monitor Events​

WebSocket: wss://api.hestia.local/ws/devices/{deviceId}