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â
- Overview
- Device Discovery
- Device State Management
- Claim States
- Active Lifecycle
- Provisioning Status
- Trust Tier Display
- Descriptor Display
- Session Expiration
- 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}