# MDK Docs (/) } title={Learn what MDK is} href="/concepts" description={ Product overview, architecture, and the MDK packages } /> } title={Get started path picker} href="/tutorials" description={ Discover your path, including backend tools and the raw client } /> } title={Ship a dashboard fast} description={ <> Two ways to ship a mining dashboard: Build with your AI agent{' '} Browse the UI Devkit{' '} } /> } title={Use UI Foundation without a dashboard} href="/guides/ui/use-ui-foundation-headlessly" description={ @tetherto/mdk-ui-foundation is headless: Zustand stores and a QueryClient factory you can wire into any runtime } /> I'm an AI agent |{' '} I am building with one } subtitle="Optimize your development workflow by bridging the gap between large language models and high-performance mining infrastructure." > } title={Build dashboards with your AI agent} href="/quickstart/connect-agents" description={ Wire your LLM to MDK with the UI CLI, then build from plain-language prompts } /> } title={Full docs in one file} href="/llms-full.txt" description={ Every page of these docs in one plain-text file—open in the browser to copy or save } /> # About MDK (/concepts) ## Introducing MDK MDK, the Mining Development Kit, is an [open-source platform](/support/community/contributing#licensing) that delivers a modern, transparent, and modular infrastructure for Bitcoin mining operations. MDK enables Bitcoin mining operations to start small, scale smoothly, and remain in full control, without lock-in, rewrites, or hidden complexity. ## The problem The Bitcoin mining industry has long been constrained by closed systems, proprietary tooling, and vendor lock-in. MDK changes that. ## The solution MDK delivers a modular mining stack that empowers operators and developers to build, monitor, control, and scale mining operations with full ownership: from a single device to gigawatt-scale facilities — without architectural rewrites. MDK ships three packages: 1. [Orchestration kernel (Kernel)](#the-orchestration-kernel). 2. [Universal SDK](#the-universal-sdk). 3. [MDK App Toolkit](#mdk-app-toolkit). All three communicate through the **MDK protocol**. Clients — browsers and [AI agents](#ai-ready-with-unified-intelligence) alike — reach the kernel exclusively through the Gateway, the secure entry point your team builds with the SDK. Tying everything together is a **single contract per device type**: the same [`mdk-contract.json`](/concepts/stack/workers#capability-contract) serves the UI (data labels), the orchestrator (validation rules), and AI agents (reasoning context). One file, three audiences, no drift. ### The orchestration kernel [Kernel](/concepts/stack/kernel), the Orchestration Kernel, is distributed as [`@tetherto/mdk-kernel`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md). It's the central coordination engine of MDK and serves as a controller: it knows which devices are online, routes commands to the right place, monitors health, and collects performance data. `@tetherto/mdk-kernel` communicates with devices through a standardized language called the **MDK Protocol**, a common set of messages that every device in the system understands, regardless of manufacturer or model. Adding a new device type never impacts `@tetherto/mdk-kernel` thanks to the Worker, a device-specific translator that sits between the kernel and your hardware: it speaks the MDK Protocol upward, and the device's native API downward. The kernel is **pull-only**, **device-agnostic**, and **self-healing**. Learn more about the [internal modules, recovery flows, and protocol specs](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#architecture) that back those guarantees. ### The universal SDK `@tetherto/mdk-client` is the universal SDK, a connection library that applications use to talk to `@tetherto/mdk-kernel`. It serves as a universal adapter: handling all the connection details so developers can focus on building their application. - **Multi-language support**: available for Node.js, Python, Go, and more; use whatever language your team prefers - **Automatic connection handling**: manages reconnection, retries, and transport selection behind the scenes - **No lock-in**: developers bring their own stack and connect via the SDK. No framework requirements. ### MDK App Toolkit For teams that want to ship fast, the [**MDK App Toolkit**](/concepts/stack/app-toolkit) is the optional, batteries-included application layer that sits on top of `@tetherto/mdk-kernel`. It ships in three parts: - **Frontend tools**: a headless state brain ([`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation)), framework adapters ([`@tetherto/mdk-react-adapter`](/tutorials/ui/react) for React today), and a production-tested React UI Kit ([`@tetherto/mdk-react-devkit`](/tutorials/ui/react)) for dashboards. - **Backend tools**: a plug-and-play library that drops into Fastify or Express to handle JWT auth, RBAC, and command proxying, with hooks for custom routes and aggregations. - **Plugins**: drop-in modules that pair a frontend tools widget with a backend tools route, so third parties can ship whole features without forking the Gateway. Using [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) without the Gateway is technically possible but not supported by this monorepo — most applications build on the Gateway. ## Who MDK is for MDK is built for everyone involved in mining Bitcoin: - **Mining operators**: monitor and control fleets with real-time dashboards. Get fleet-wide summaries (total hashrate, power usage, temperature alerts) across all your sites. - **Hardware manufacturers**: integrate new devices by building a Worker and writing one [`mdk-contract.json`](/concepts/stack/workers#capability-contract). No involvement from MDK maintainers needed. - **Software developers**: build custom mining applications in any language, or leverage the [MDK App Toolkit](/concepts/stack/app-toolkit)'s frontend and backend tools for rapid development. - **AI/Automation teams**: [connect intelligent agents](#ai-ready-with-unified-intelligence) that can monitor, diagnose, and act on device issues autonomously ## Architecture overview `@tetherto/mdk-kernel` is [the kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md). [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) is the protocol connector every caller uses to reach it. Above those two layers, the supported development path builds in two levels: - **Gateway**: the [Gateway](/concepts/stack/gateway) wraps `@tetherto/mdk-client` and adds [authentication](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md#security-model), [RBAC](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md#security-model), fleet aggregation, and an HTTP/WebSocket/MCP interface. AI agents drive the fleet through its MCP endpoint - **MDK App Toolkit**: sits on top of the Gateway. Adds a plugin system for declarative route extensions and frontend packages ([`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation), React adapter, React UI kit) for teams building operator dashboards Below the kernel, **devices are the source of truth**. The actual hardware state is reported by the Worker to `@tetherto/mdk-kernel`, which orchestrates a synchronized view across the fleet. For the full layer-by-layer view with transports and discovery flows, see the [MDK stack](/concepts/architecture#mdk-stack) on the Architecture page. ## AI-ready with unified intelligence MDK is designed from the ground up for [AI-driven operations](/concepts/architecture#ai-agents-and-the-mcp-server). Rather than bolting AI on as an afterthought, intelligence is woven directly into the device definition itself. In addition to the technical schemas, every device's contract file ([`mdk-contract.json`](/concepts/stack/workers#capability-contract)) contains: - **Safety rules**: for example, "Outlet temperature > 85°C requires immediate intervention" - **Operational constraints**: limits on command frequency, power thresholds, cooling requirements - **Troubleshooting guides**: if/then recovery steps that AI agents can follow autonomously This means an AI agent connecting to MDK doesn't need a separate knowledge base or custom prompts per device. The intelligence travels with the device; the same contract that validates commands and generates dashboards also determines how AI reasons about that hardware. ## What you can build - Operational dashboards (hashrate, power, temperature) - Multisite fleet management with centralized oversight - Alerts and notifications for critical device events - Overheating detection and automated remediation - AI-driven autonomous monitoring and control - Custom analytics and reporting pipelines - White-labeled hosted mining platforms - Third-party device integrations and plugins ## Scaling MDK [scales](/concepts/architecture#scaling) naturally without architectural changes: - **More devices?** Add more Workers. Each Worker owns a specific set of devices, and `@tetherto/mdk-kernel` routes commands to the right one automatically. - **More sites?** Each physical site runs its own `@tetherto/mdk-kernel` instance. A single Gateway connects to all of them, giving you one view across your entire operation. - **Site isolation**: `@tetherto/mdk-kernel` instances are fully independent. A problem at one site has zero impact on any other. ## Next steps Learn more about: - [Architecture](/concepts/architecture) - [MDK App Toolkit](/concepts/stack/app-toolkit) - [Connecting intelligent agents](/concepts/architecture#ai-agents-and-the-mcp-server) # How agents build with MDK (/concepts/agents) MDK is built so your AI coding agent can turn a plain-language prompt into a working dashboard, without you wiring components by hand. This page explains *why* those results are trustworthy — the build-time flow an agent follows on your machine. For the setup steps, see [Build dashboards with your AI agent](/quickstart/connect-agents). For the *runtime* path where agents drive a live fleet, see [AI agents and the MCP Server](/concepts/architecture#ai-agents-and-the-mcp-server). ## What your agent does for you Behind a single prompt, the agent: - Finds the right MDK components and hooks for your intent - Wires real adapter hooks and state, with no guessed imports or props - Scaffolds a page and verifies that it compiles - Reaches for stable, supported components by default, so the result is something you can ship The outcome: the agent pulls real metadata, real examples, and real types instead of hallucinating an API. ## How it works MDK ships a small set of machine-readable files that describe every component, hook, and store. Your agent reads those local files, makes no network or model calls of its own to discover them, and only reaches for exports that MDK marks as stable. Because it works from generated metadata rather than guesswork, it does not invent props or imports. ```mermaid flowchart LR prompt["Your prompt"] agent["AI agent"] manifests["MDK local manifests"] page["Scaffolded, verified page"] prompt --> agent agent --> manifests manifests --> agent agent --> page ``` For the full command surface those manifests power, see the [UI CLI reference](/guides/ui/ui-cli). The complete contract that keeps the metadata honest lives in the [MDK repositories](/support/resources/repositories). ## Next steps - [Build dashboards with your AI agent](/quickstart/connect-agents): the two-step setup flow - [Build a dashboard with an agent](/tutorials/ui/react/build-any-dashboard-with-an-agent): a full Stats Lab walkthrough with the UI CLI commands - [UI CLI reference](/guides/ui/ui-cli): every command your agent (or you) can run - [AI agents and the MCP Server](/concepts/architecture#ai-agents-and-the-mcp-server): the runtime path for live fleet control # Architecture (/concepts/architecture) Status: 🚧 MDK is in active development. This page describes the target architecture and may evolve as real-world implementations land. ## How MDK works MDK is built around a small kernel with one job: route validated commands to whichever Worker owns a device, and pull telemetry back. Everything else (authentication, business logic, UI, AI agents) sits outside the kernel as composable layers: keeping the kernel small and the application surface open. To prevent unbound flexibility from manifesting as system rigidity, the architecture draws a hard line between what is standardized and what is delegated. It's: - **Opinionated where needed**: strict transport envelopes, unified JSON schema, unidirectional flows - **Flexible where it matters**: isolated Workers handle translation logic, enabling integrations without polluting the core infrastructure Five layers compose the stack, with strict, unidirectional flows between them. The kernel itself is **Kernel**, the Orchestration Kernel, distributed as `@tetherto/mdk-kernel`. ## MDK stack ```mermaid graph TB subgraph consumers ["Layer 1: Consumers"] UI["UI / Frontend"] AI["AI Agent"] end subgraph gateway ["Layer 2: Gateway"] WebApp["HTTP / API Router"] MCPServer["MCP Server Endpoint"] end subgraph kernelLayer ["Layer 3: @tetherto/mdk-kernel"] Kernel["Kernel
• Command routing
• Health monitoring
• Device registry
• Telemetry collection"] end subgraph workers ["Layer 4: Workers"] Workers["WORKERS"] end subgraph devices ["Layer 5: Physical Devices"] Devices["Physical devices
• Miners
• Containers
• Sensors"] end UI -->|"HTTP / WebSocket"| WebApp AI -->|"MCP Protocol"| MCPServer WebApp -->|"MDK Protocol via @tetherto/mdk-client / HRPC"| Kernel MCPServer -->|"MDK Protocol via @tetherto/mdk-client / HRPC"| Kernel Workers -.->|"join known DHT topic"| Kernel Kernel -->|"MDK Protocol: pull (identity / schema / telemetry) + command"| Workers Workers -->|"device libs"| Devices style consumers fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style gateway fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style kernelLayer fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style workers fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style devices fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` The MDK components that compose those layers: | Component | What it does | |---|---| | [`@tetherto/mdk-kernel`](#the-kernel) | Central coordination: routes commands, collects telemetry, monitors health | | [`@tetherto/mdk-client`](#the-sdk) | Universal SDK applications use to talk to `@tetherto/mdk-kernel` | | [MDK Protocol](#the-mdk-protocol) | Standardized message envelope every layer speaks | | [MDK App Toolkit](/concepts/stack/app-toolkit) | Optional frontend tools, backend tools, and plugins on top of `@tetherto/mdk-kernel` | ## Storage [Hypercore](https://github.com/holepunchto/hypercore)-backed stores (such as [Hyperbee](https://github.com/holepunchto/hyperbee)) are recommended across the `@tetherto/mdk-kernel`, Worker, and Gateway layers. This choice satisfies all storage requirements without the operational baggage of a centralized database. ## The MDK protocol The MDK protocol is the contract that crosses every layer of the stack. Workers become reachable — via a [DHT topic](/concepts/stack/workers#dht-mode) or [same-machine discovery](/concepts/stack/workers#local-mode), and `@tetherto/mdk-kernel` initiates every RPC call. Workers issue no callbacks, emit no fan-out events, and make no exceptions to the direction of flow. For the full [envelope schema](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/lib/protocol/envelope.js), [action catalogue](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/lib/protocol/actions.js), and [base command set](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/lib/protocol/schemas.js), see the [Protocol reference](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md). ### Design principles - **Transport-agnostic**: identical messages whether routed [in-process](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/lib/transport/envelope-router.js), over [Hyperswarm RPC (HRPC)](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/lib/transport/hrpc-listener.js), or via API calls - **Strictly unidirectional**: [Workers](/concepts/stack/workers) never initiate RPC calls to `@tetherto/mdk-kernel`; `@tetherto/mdk-kernel` discovers their presence and initiates all subsequent communication downwards (identity, capabilities, telemetry, commands) - **Generic interface**: the accepted interface is defined dynamically at the Worker level via a self-describing capabilities schema containing both structure and semantic context for AI agents ### Governance To maintain structural integrity and contract stability across `@tetherto/mdk-kernel`, Gateway, and Workers, MDK protocol messages are governed and strictly validated using [Hyperschema](https://github.com/holepunchto/hyperschema). Hyperschema also aligns natively with the system's underlying Hyperbee storage. ### Discovery, telemetry, and command flows ```mermaid sequenceDiagram participant W as Worker participant DHT as DHT Topic (Hyperswarm) participant O as @tetherto/mdk-kernel participant G as Gateway (HTTP / MCP) Note over W,O: Worker discovery and registration W->>DHT: Joins known topic O-->>DHT: Detects new peer connection O->>W: identity.request W-->>O: identity.response (devices) O->>O: Save Worker to registry O->>W: capability.request W-->>O: capability.response (schema) Note over O,W: Telemetry pull loop O->>W: telemetry.pull W-->>O: metrics and pending commands Note over G,W: Command execution G->>O: MDK Protocol HRPC envelope O->>W: command.request (routed by deviceId) W-->>O: command.result O-->>G: result ``` ## The Kernel [Kernel](/concepts/stack/kernel), [`@tetherto/mdk-kernel`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md), is the trusted coordination layer at the heart of MDK. It [routes commands](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#commanddispatcher), [monitors device health](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#healthmonitor), [registers Workers](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#workerregistry), and [pulls telemetry](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#telemetrycollector) — all on a [pull-only model](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#scheduler), so the kernel cannot be overwhelmed by upstream pressure. When a command arrives, callers only need to provide a `deviceId`; `@tetherto/mdk-kernel` resolves the owning Worker internally via the [`CommandDispatcher`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#commanddispatcher) and dispatches the `command.request`. ## Workers [Workers](/concepts/stack/workers) wrap a device library and expose it via the MDK protocol. They are the integration handlers between physical hardware and `@tetherto/mdk-kernel`, and the unyielding source of truth for that hardware: `@tetherto/mdk-kernel` itself operates purely as a synchronized state machine over Worker-reported state. Workers are passive — Kernel initiates every RPC call; Workers only ever respond. Kernel discovers Workers according to the [discovery model](/concepts/stack/workers#discovery-model), then requests identity and capabilities. ## The SDK The [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) SDK is the transport abstraction layer used to connect to `@tetherto/mdk-kernel` reliably. It is the essential glue between the kernel and any consumer layer developers choose to build on top. **Responsibility**: connects the MDK Protocol over the native HRPC transport seamlessly, offering: - **Transport abstraction**: handles MDK Protocol message construction and reconnection logic with exponential backoff. - **Key-based addressing**: the SDK connects over encrypted Hyperswarm streams, addressed by the kernel's HRPC public key. The same transport serves remote server-to-server production and same-host development alike — for the local zero-config case, the kernel publishes its key to a well-known key file that clients read at startup. - **Major language support**: `@tetherto/mdk-client` is intended to support all major languages (Node.js, Python, Go, and others), allowing developers to dispatch commands, subscribe to live streams, or pull status snapshots from any stack. ## Gateway The [Gateway](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) wraps `@tetherto/mdk-client` — the MDK protocol connector to Kernel — to add an authenticated HTTP, WebSocket, and MCP interface on top. Consumers that need those capabilities connect through the Gateway. The supported development path is the [MDK App Toolkit](/concepts/stack/app-toolkit), which ships backend middleware (JWT auth, RBAC, and command proxying), frontend tools, and an `mdk-plugin.json`-based plugin system for declarative HTTP route extensions ([plugin guide](/guides/gateway/plugins)). For the full developer model — extension patterns, data access, auth design, and Kernel connection — read the [Gateway concept page](/concepts/stack/gateway). ## AI agents and the MCP server The supported application path connects AI agents through an **MCP endpoint** on the Gateway. This keeps agents inside the same security envelope as other consumers: they are authenticated clients subject to the same JWT validation, rate limits, and RBAC as a human user. This is intentional because Kernel does not perform user-level [authentication](/concepts/stack/gateway#authentication-design). What makes the integration distinctive is **[runtime tool derivation](https://github.com/tetherto/mdk/blob/main/docs/reference/maintainers/agent-ready-sdk.md)**. The tools exposed to an agent (for example, `get_device_telemetry` or `reboot_device`) are not hardcoded; they are parsed at runtime from each registered Worker's [`mdk-contract.json`](/concepts/stack/workers#capability-contract). When a new device type joins the network, the agent gains the ability to query and control it without any change to the Gateway. ## End-to-end data flows Two scenarios show the full request path from consumer to device and back: a [human user clicking through the UI](#human-ui-scenario), and an [AI agent executing a multi-step prompt](#ai-agent-scenario). ### AI agent scenario A user instructs the AI Agent: *"Keep the fleet healthy."* The agent monitors continuously, catches `wm002` overheating, reboots it, and notifies the user. ```mermaid sequenceDiagram actor User participant AI as AI Agent participant Node as Gateway (MCP) participant Kernel as @tetherto/mdk-kernel participant Worker as Generic Worker User->>AI: "Keep the fleet healthy." Note over AI,Kernel: Step 1: Fleet discovery (read) AI->>Node: Call MCP tool get_fleet_alerts (token auth) Node->>Node: Validate agent token and RBAC Node->>Kernel: HRPC query (via @tetherto/mdk-client) Kernel-->>Node: Metrics Node-->>AI: Tool result (wm002 is overheating) Note over AI,Kernel: Step 2: Execution (write) AI->>Node: Call MCP tool reboot_device (deviceId wm002) Node->>Node: Validate token and device:write RBAC Node->>Kernel: dispatch generic protocol message Kernel->>Kernel: Resolve deviceId Kernel->>Worker: command.request (HRPC) Worker-->>Kernel: command.result Kernel-->>Node: result OK Node-->>AI: Tool result (Success) AI-->>User: "wm002 was overheating and has been rebooted." ``` ### Human UI scenario A user clicks "Reboot" on device `wm001` in the UI. ```mermaid sequenceDiagram actor User participant UI as React UI participant Node as Gateway participant Kernel as @tetherto/mdk-kernel participant Worker as Generic Worker User->>UI: Click "Reboot" on wm001 UI->>Node: POST { `deviceId`, action, payload } Note over Node,Kernel: Delegation Node->>Kernel: dispatch generic protocol message Kernel->>Kernel: Verify against capabilities Kernel->>Kernel: Resolve Worker for `deviceId` Note over Kernel,Worker: Execution Kernel->>Worker: command.request (HRPC) Worker-->>Kernel: Ack start Worker->>Worker: Hardware-specific translation Worker-->>Kernel: command.result Kernel-->>Node: result OK Node-->>UI: HTTP 200 Note over Worker,Kernel: State reflection Kernel->>Worker: telemetry.pull (tick) Worker-->>Kernel: Updated status (rebooting) ``` The Gateway, Kernel, and Workers, [control plane includes approval-gated writes](/concepts/control-plane). ## Scaling As MDK deployments scale to large mining sites (5,000+ devices), the system must explicitly manage parallel Workers and parallel `@tetherto/mdk-kernel` instances. The kernel is only an execution layer; it does not perform application-level aggregation or cross-regional business logic. Scaling here means *how many* Workers and kernels you run. That is independent of [deployment topology](/concepts/deployment-topologies), *how those processes are packaged* on a host (one process vs. many). ### Parallel Workers Multiple Workers of the same type (for example, `whatsminer-worker`) can be active concurrently and connected to the same `@tetherto/mdk-kernel` kernel. ```mermaid flowchart TD subgraph kernel ["Single @tetherto/mdk-kernel instance"] Kernel["Kernel"] end W1["Worker 1"] W2["Worker 2"] D1["Devices wm001 to wm500"] D2["Devices wm501 to wm999"] Kernel -->|Routes commands| W1 Kernel -->|Routes commands| W2 W1 --- D1 W2 --- D2 style kernel fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` **Device-level routing and ownership**: Workers never share devices. When a Worker connects, its `identity.register` payload explicitly lists the `deviceId`s it exclusively manages. The Worker registry maintains this strict mapping and deterministically routes arriving commands to the designated Worker. ### Multi-site deployments A deployment may need to manage multiple massive physical boundaries (for example, a Texas Site and an Iceland Site). Each location runs its own dedicated site-level `@tetherto/mdk-kernel` kernel, but all are overseen globally by a single Gateway and AI Agent. ```mermaid flowchart TD Global["Global Gateway / AI Agent"] subgraph texas ["Texas site"] KERNEL_TX["@tetherto/mdk-kernel"] W1_TX["Whatsminer Worker"] W2_TX["Antminer Worker"] D1_TX["Whatsminers"] D2_TX["Antminers"] KERNEL_TX -->|Routes| W1_TX KERNEL_TX -->|Routes| W2_TX W1_TX --- D1_TX W2_TX --- D2_TX end subgraph iceland ["Iceland site"] KERNEL_IC["@tetherto/mdk-kernel"] W1_IC["Whatsminer Worker"] W2_IC["Avalon Worker"] D1_IC["Whatsminers"] D2_IC["Avalons"] KERNEL_IC -->|Routes| W1_IC KERNEL_IC -->|Routes| W2_IC W1_IC --- D1_IC W2_IC --- D2_IC end Global <-->|MDK Protocol via HRPC| KERNEL_TX Global <-->|MDK Protocol via HRPC| KERNEL_IC style texas fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style iceland fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` The single Gateway and AI Agent connect globally to all distributed `@tetherto/mdk-kernel` kernels via the native HRPC mesh (Hyperswarm). Parallel `@tetherto/mdk-kernel` instances remain entirely isolated from one another: they do not federate registries, share queues, or synchronize state. A crash at one site has zero impact on any other. Cross-site aggregation is handled purely at the Gateway layer, where routes query multiple Workers via `@tetherto/mdk-kernel` and merge the responses before returning them to the UI or Agent. ## Next steps - Understand the [Kernel](/concepts/stack/kernel) — what it owns, the pull-only model, and transports - Understand the [Gateway](/concepts/stack/gateway) — authentication, RBAC, plugins, and Kernel connection - Understand [Workers](/concepts/stack/workers) — discovery model, capability contract, and adding hardware - Understand the [control plane](/concepts/control-plane) — how Gateway, Kernel, and Workers communicate and which layer owns each responsibility - Choose a [deployment topology](/concepts/deployment-topologies) — single-process, local, or distributed ## Next steps Learn more about: - [About MDK](/concepts) - [MDK App Toolkit](/concepts/stack/app-toolkit) - [Kernel](/concepts/stack/kernel) - [Get started](/quickstart) # Control plane (/concepts/control-plane) ## Overview This page covers authenticated requests, live reads, command dispatch, and approval-gated writes. It spans the Gateway, Kernel, and Workers, but each layer owns a different responsibility. Use this page to understand which layer receives a request, which layer validates it, and when a write becomes a command. For package-level APIs and configuration, use the [Gateway README](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md), [Kernel README](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md), and [Worker README](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md). ## Responsibility boundaries **Gateway** owns the consumer-facing surface, including HTTP, WebSocket, MCP, plugins, authentication, and RBAC. Browser UIs and agents should enter MDK through the Gateway (they do not talk to Kernel directly). **Kernel** owns coordination: Worker registry, telemetry routing, health checks, command dispatch, command state, and the write-action approval modules. Kernel trusts established callers; it does not validate user identity. **Workers** own hardware integration. They declare capabilities, answer Kernel-initiated telemetry and state pulls, resolve candidate write calls for approval-gated actions, and execute final commands against devices. ## Connection direction The direction of each connection is intentional: - Consumers call the Gateway over HTTP, WebSocket, or MCP - The Gateway dials Kernel over [Hyperswarm RPC (HRPC)](/reference/glossary#hyperswarm-rpc) through `@tetherto/mdk-client` - Kernel discovers Workers, then initiates every Worker RPC - Workers never initiate upstream calls to Kernel or the Gateway The [deployment topologies](/concepts/deployment-topologies) and [Workers discovery model](/concepts/stack/workers#discovery-model) pages cover how this changes across single-process, local, and distributed deployments. ## Transport identity and admission HRPC uses encrypted Noise connections with public-key identities. Kernel's HRPC public key identifies and addresses the Kernel listener. Each caller has a separate public key that the listener receives during connection setup. Kernel compares the caller's key with the allowlist. An empty allowlist admits any HRPC caller; a configured allowlist admits the approved callers. This transport-level check works the same way whether the processes share a host or when they communicate across a network. Transport identity is not user identity. The HRPC allowlist controls which backend processes may connect to Kernel; the Gateway separately validates JWTs and enforces RBAC for people, browser applications, and agents. ## Request paths ### Read requests Reads usually start in a Gateway route or plugin controller, pass through `services.mdkClient`, and reach Kernel as registry, capability, telemetry, or state queries. Kernel routes Worker-owned reads down to the relevant Worker and returns the result to the Gateway. Gateway controllers can combine live Kernel data with persisted local data from `services.dataProxy`. For plugin controller mechanics, use the [Gateway plugins guide](/guides/gateway/plugins). ### Direct commands Direct commands are immediate writes that do not require approval. The Gateway validates the request and RBAC at the route layer, then sends a `command.request` to Kernel. Kernel resolves the owning Worker, validates the command against the Worker's capabilities, and hands the command to the crash-recoverable command state machine. For command-dispatch module details, use the [Kernel README](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md). ### Approval-gated writes Some writes are staged for approval before they become commands. This keeps direct commands available while adding a separate review path for fleet-changing actions that need operator approval. ```mermaid flowchart TB directCommand["Direct command"] --> commandRequest["command.request"] commandRequest --> dispatcher["CommandDispatcher"] dispatcher --> stateMachine["CommandStateMachine"] stateMachine --> worker["Worker write"] writeAction["Approval-gated write action"] --> actionPush["action.push"] actionPush --> actionManager["ActionManager"] actionManager --> actionApprover["ActionApprover / voting store"] actionApprover --> approved{"Approved?"} approved -->|"yes"| actionCaller["ActionCaller"] approved -->|"no"| stopped["Rejected or cancelled"] actionCaller --> commandRequest ``` The Gateway owns the `/auth/actions*` HTTP surface and checks route-level RBAC such as `actions:w`. Kernel owns `ActionManager`, `ActionCaller`, and target permission checks at the protocol layer. Those Kernel checks use the target Worker's device family, such as `miner:w` or `container:w`, before resolving or approving writes. Workers answer `write.calls.request` while Kernel resolves candidate writes, then execute the final `command.request` after the configured vote thresholds are met. For implementation steps, use the [write-actions how-to](/guides/gateway/write-actions). For React hook names and exports, use the [React adapter README](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md). ## Developer surfaces The write-action flow is reachable from two different layers depending on where you are building. | Layer | Package | How you call it | |---|---|---| | React / UI | [`@tetherto/mdk-react-adapter`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md) | Six hooks: `useSubmitSingleAction`, `useSubmitPendingActions`, `useVoteOnAction`, `useCancelAction`, `usePendingActions`, `useLiveActions` — call the Gateway `/auth/actions*` routes | | Backend / Node.js | [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) | Methods: `pushAction`, `pushActionsBatch`, `voteAction`, `cancelActionsBatch`, `getAction`, `getActionsBatch`, `queryActions` — send MDK Protocol envelopes directly to Kernel | The React hooks go through the Gateway, which enforces JWT validation and RBAC (`actions:w`) for every request. The `mdk-client` methods connect directly to Kernel and bypass those user-level controls. The Kernel admits backend processes according to its HRPC transport policy: an empty allowlist admits any HRPC caller, while a configured allowlist admits matching caller keys. ## Next steps - Build Gateway routes with the [plugin guide](/guides/gateway/plugins) - Submit and approve write actions with the [write-actions how-to](/guides/gateway/write-actions) - Review the [Kernel modules](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md) - Review Worker capabilities in the [Worker README](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md) ## Next steps - [Get started](/quickstart) Learn more about: - [About MDK](/concepts) - [MDK App Toolkit](/concepts/stack/app-toolkit) - [Kernel](/concepts/stack/kernel) # Deployment topologies (/concepts/deployment-topologies) This page explains the three supported deployment shapes and when to pick each. ## Overview MDK's runtime pieces — the [Kernel](/concepts/architecture), the Gateway, and one or more Workers — can run together in a single process or be split across several. This is a **packaging and operations** choice, and it's independent of how MDK [scales logically](/concepts/architecture#scaling) (adding Workers, adding sites). If Kernel, Worker, manager, or thing are unfamiliar, read the [`glossary.md`](/reference/glossary) first. ## Connection model Before choosing a shape, it helps to understand which components initiate connections: - The Gateway dials Kernel — it is the active side of that connection, over [Hyperswarm RPC (HRPC)](/reference/glossary#hyperswarm-rpc) using the Kernel's public key (read from the well-known key file on the same host, or passed as `kernelKey` for a remote host) - Kernel discovers Workers and initiates every RPC call — Workers are passive; they become reachable and wait - Workers never initiate any connection This directionality is what drives the transport and discovery configuration in each shape below. For detail, see the [Workers discovery model](/concepts/architecture#workers) and the [Gateway Kernel connection](/concepts/stack/gateway#kernel-connection). ## The three shapes ### Single process ```mermaid flowchart LR classDef mdk fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A sApp["Gateway"]:::mdk -->|"HRPC"| sKernel["Kernel"]:::mdk sKernel -.->|"in-process"| sW1["Worker A"]:::mdk sKernel -.->|"in-process"| sW2["Worker B"]:::mdk ``` *Solid arrow: active connection initiated by the source. Dashed arrow — Kernel-initiated discovery.* Kernel, the Gateway, and every Worker run inside one Node.js heap and event loop. Lowest footprint, simplest to start, nothing external to supervise. This is the shape behind the [single-process site how-to](/guides/deployment/run-single-process-site). ### Local ```mermaid flowchart LR classDef mdk fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A lApp["Gateway"]:::mdk -->|"HRPC"| lKernel["Kernel"]:::mdk lKernel -.->|"shared dir"| lW1["Worker A"]:::mdk lKernel -.->|"shared dir"| lW2["Worker B"]:::mdk ``` *Solid arrow: active connection initiated by the source. Dashed arrow — Kernel-initiated discovery.* Each service runs as its own OS process on the same machine. Kernel discovers Workers via a shared directory — no DHT configuration needed. The [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md#how-out-of-process-workers-find-the-kernel) runs in local mode by default. ### Microservices ```mermaid flowchart LR classDef mdk fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A mApp["Gateway (host 1)"]:::mdk -->|"HRPC"| mKernel["Kernel (host 2)"]:::mdk mKernel -.->|"DHT"| mW1["Worker A (host 3)"]:::mdk mKernel -.->|"DHT"| mW2["Worker B (host N)"]:::mdk ``` *Solid arrow: active connection initiated by the source. Dashed arrow — Kernel-initiated discovery.* Each service runs as its own OS process or container, potentially on separate hosts, supervised by pm2 or Docker and connected via DHT. This is the shape behind the [microservices site guide](/guides/deployment/run-microservices-site). ## The trade-off Pick **single-process** when: - You are developing locally, running demos, or want a self-contained site for tests - Footprint matters more than isolation (minimal or embedded deployments) - You do not need supervisor-managed restarts Pick **local** when: - All services run on one machine and you want independent process restarts - Outbound networking is restricted removing DHT as an option - You want process isolation and independent restarts without the complexity of DHT Pick **microservices** when: - You want to allocate resources per service — CPU and memory limits per process or container - Workers run on separate hosts from Kernel or the Gateway - You are orchestrating many Workers across one or more hosts ## Where `worker.js` fits The microservices shape is built on [`backend/core/mdk/worker.js`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/worker.js), a shared process entry compatible with pm2, Docker, or a direct `node worker.js`. It is driven by environment variables (`SERVICE`, and for a Worker `WORKER`/`TYPE`/`RACK`) rather than CLI flags. One `worker.js` runs per service, and the supervisor (pm2 or Docker) owns its lifecycle and resource limits. The [standalone `worker.js` install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md#standalone-via-workerjs) defines the per-Worker mechanics. The single-process and local shapes both call the programmatic APIs (`getKernel`, `startWorker`, `startGateway`) directly. Local mode passes `discovery: { mode: 'local' }` to both `getKernel` and `startWorker` so they coordinate via a shared directory rather than DHT — see [local Worker discovery](/concepts/stack/workers#local-mode) for configuration options. ## Relationship to scaling Topology is orthogonal to scale. [Logical scaling](/concepts/architecture#scaling) is about *how many* Workers and Kernel kernels you run (parallel Workers, per-site kernels, multi-site oversight). Deployment topology is about *how those processes are packaged* on a given host. You choose both: for example, a production site typically runs multiple processes (this page) and multiple parallel Workers per kernel ([scaling](/concepts/architecture#scaling)). ## Next steps - Run a self-contained local site: [Single-process site](/guides/deployment/run-single-process-site) - Run [same-machine services without DHT](/concepts/stack/workers#local-mode) - Run [supervised services on one or more hosts](/guides/deployment/run-microservices-site) - Register [one miner before packaging a whole site](/guides/miners) # Security boundaries (/concepts/security-boundaries) 🚧 This page is under construction: more data to follow. ## Worker security boundary `WorkerRuntime` listens over HyperswarmRPC. Its underlying HyperDHT connection uses encrypted Noise transport, and the Worker's HRPC public key identifies and addresses that Worker endpoint. This authenticates the endpoint to the connecting backend peer; it does **not** establish a human or application identity, grant command permission, or replace Gateway authentication and RBAC. The current `WorkerRuntime` does not enforce a caller allowlist before dispatching supported envelopes. Any backend peer that can reach the Worker and address its public key may send requests. Kernel's HRPC caller allowlist protects clients connecting to Kernel; it does not authorize direct callers to a Worker endpoint. Consumers must enter through the authenticated Gateway → Kernel path, and direct Worker reachability must be restricted to trusted backend networks. Treat Worker public keys and DHT topics as deployment configuration, distribute them through an authenticated control plane, apply host/container firewall policy, and never expose device management interfaces publicly. DHT topics provide rendezvous only; they are not credentials or authorization tokens. The minimal host passes `services: null`. It therefore does not provide first-party service built-ins or `write.calls.request` approval integration — see Worker Runtime legacy services for the full built-in surface an `opts.services` object can activate. Direct `command.request` dispatch still reaches plugin command handlers. Production command paths must authenticate the requester at the Gateway/control plane, authorize each device and command, optionally require approval for high-impact actions, validate again in the handler, rate-limit, and create an audit record containing actor, target, requested parameters, outcome, and correlation ID. The handler context does not currently include actor identity, so actor-level auditing belongs upstream; handler logs supplement it. See the [control-plane security model](/concepts/control-plane) for the production trust path. Inject credentials through the host process from a secret manager or protected environment, pass only the minimum device-specific values in `config`, never place secrets in `mdk-contract.json`, and redact credentials and device responses from errors, debug logs, telemetry, and audit records. # Stack (/concepts/stack) MDK's backend is composed of four coordinated layers. Each layer has a single, bounded responsibility; together they form a complete path from physical device to application consumer. | Layer | What it owns | | --- | --- | | [Workers](/concepts/stack/workers) | Device integration — translate hardware telemetry and commands into the MDK Protocol | | [Kernel](/concepts/stack/kernel) | Kernel — route commands, monitor health, register Workers, and pull telemetry | | [Gateway](/concepts/stack/gateway) | Application gateway — authenticated HTTP, WebSocket, and MCP interface on top of Kernel | | [MDK App Toolkit](/concepts/stack/app-toolkit) | Development toolkit — Gateway backend, plugin system, and frontend packages | For the architecture overview and how data flows between layers, see [Architecture](/concepts/architecture). # MDK App Toolkit (/concepts/stack/app-toolkit) ## Overview The MDK App Toolkit is the recommended development path for teams building MDK-powered applications. It is composed of three coordinated layers: - Gateway backend - Plugin system - Frontend packages Not every layer is required for every consumer type. MDK supports two primary consumer patterns: - **Human operator UI**: a frontend application connects to the Gateway's [REST](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md#http-api-overview) and [WebSocket](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md#websocket-subscriptions) APIs. The full three-layer toolkit applies — Gateway, plugin system, and frontend packages - **AI agent / headless consumer**: an AI agent connects to the Gateway's MCP endpoint and subscribes to telemetry feeds directly. The frontend packages are not required; the Gateway and plugin system alone are sufficient Status: MCP is in progress. The HTTP and WebSocket consumer paths are available today. ## Gateway layer `@tetherto/mdk-gateway` is the backend component of the toolkit. It wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) — the Kernel protocol connector — and delivers an authenticated HTTP, WebSocket, and MCP interface for consumers that need those capabilities. Read the [Gateway concept page](/concepts/stack/gateway) for the full developer model: extension patterns, data access, auth design, and Kernel connection. As a toolkit component, the Gateway provides out of the box: - Fastify-based HTTP server and WebSocket endpoint - JWT authentication, session management, and OAuth2 (Google and Microsoft) - RBAC enforcement at the route level - Command proxying and telemetry subscriptions to Kernel via `@tetherto/mdk-client` - MCP endpoint for AI agents Using [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) without the Gateway runtime is technically possible — you write your own auth, routing, and middleware — but it is not supported by this monorepo. Most applications build on the Gateway. ## Plugin system `@tetherto/mdk-plugins` is the extension mechanism. A plugin is a directory containing an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) manifest and one or more controller files. The Gateway discovers and loads plugins from directories passed via [`extraPluginDirs`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#mounting-plugins). The toolkit ships defaults plugins, e.g., `auth` (user authentication routes), `telemetry` (hashrate, efficiency, temperature metrics), and `site-hashrate` (aggregated site history). Any plugin you write loads identically. - [Plugin authoring guide](/guides/gateway/plugins) — build process, manifest schema, controller contract - [Plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) — manifest schema, default routes, loader errors ## Frontend packages These packages are for the **human operator UI** pattern — the application layer that connects to the Gateway's REST and WebSocket APIs. If your consumer is an AI agent connecting via the MCP endpoint, this layer is not required. Early versions of MDK ship three layered workspace packages within the monorepo. npm packages will be published as the tooling matures. Consuming applications add the workspace dependencies directly. Consuming the whole chain is the recommended path for operator UIs. The [UI architecture reference](https://github.com/tetherto/mdk/blob/main/ui/docs/ARCHITECTURE.md) covers the full dependency graph, build strategy, and package internals. **[`@tetherto/mdk-ui-foundation`](https://github.com/tetherto/mdk/blob/main/ui/packages/ui-foundation/README.md)**: framework-agnostic headless core. No React imports. Provides Zustand vanilla stores (`authStore`, `devicesStore`, `notificationStore`, `timezoneStore`, `actionsStore`), a TanStack `QueryClient` factory with environment-aware base URL resolution, centralised `queryKeys` and query factories for all read endpoints (including Op Centre reads — site, racks, PDU layout, global data, `thingConfig` — and Pool Manager), Op Centre query parameter builders, the per-model container detail-tab matrix, a null-safe envelope flattener (`flattenKernelEnvelope`), and the Gateway API type contracts. **[`@tetherto/mdk-react-adapter`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md)**: React bindings for the core. Provides `` (required at the app root) and store hooks (`useAuth`, `useDevices`, `useTimezone`, `useNotifications`, `useActions`). **[`@tetherto/mdk-react-devkit`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/README.md)**: React UI library. `src/primitives/` ships generic UI primitives built on Radix UI (Button, Dialog, Switch, Select, Data Table, Charts). `src/domain/` ships mining-domain components, features, and presentation hooks. ### Developer entry points The toolkit can be adopted at any of the following entry points, from most batteries-included to least. | Entry point | Package | What ships | What you write | When to choose | |---|---|---|---|---| | UI Kit | `@tetherto/mdk-react-devkit` (`/primitives` + `/domain` entrypoints) | Pre-built React components, shell layout, ready-made ops dashboard | Data wiring, optional theming | You want a dashboard up fast | | Framework adapter | `@tetherto/mdk-react-adapter` (React today; Vue/Svelte/WC planned) | ``, store hooks, TanStack Query re-exports | Your own components and layout | You have a design system already | | UI Foundation | [`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation) | Zustand vanilla stores, `QueryClient` factory, `queryKeys`, query factories, Op Centre query builders, container tab matrix, API types | Framework bindings or headless utilities | You need store access outside React or are building a new adapter | | Raw SDK | `@tetherto/mdk-client` | MDK Protocol client, connection management, reconnection | Everything above the wire: state, framework, UI | You are building a non-UI consumer (CLI, agent, backend service) | ## Architecture overview ```mermaid flowchart TD subgraph frontend ["Frontend packages"] direction TB UI_FOUNDATION["@tetherto/mdk-ui-foundation (headless stores)"] FRAMEWORKS["@tetherto/mdk-react-adapter (React bindings)"] UI_COMPS["@tetherto/mdk-react-devkit (UI Kit)"] UI_COMPS -->|consumes adapter hooks| FRAMEWORKS FRAMEWORKS -->|binds headless stores| UI_FOUNDATION end subgraph backend ["Gateway + plugins (server)"] direction TB PLUGINS["@tetherto/mdk-plugins (default + custom routes)"] ROUTER["@tetherto/mdk-gateway (HTTP / WS / MCP)"] CLIENT["@tetherto/mdk-client (protocol connector)"] PLUGINS -->|registers routes into| ROUTER ROUTER -->|proxies to Kernel via| CLIENT end UI_FOUNDATION <-->|"HTTP / WebSocket"| ROUTER CLIENT -->|"MDK Protocol"| Kernel["@tetherto/mdk-kernel (kernel)"] style frontend fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style backend fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` ## Next steps - Understand the [Gateway surface](/concepts/stack/gateway) - [Build or extend with the plugin system](/guides/gateway/plugins) - Explore the [frontend package architecture](https://github.com/tetherto/mdk/blob/main/ui/docs/ARCHITECTURE.md) # Gateway (/concepts/stack/gateway) ## Overview This page introduces the [Gateway](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) surface. It explains what concerns it owns, how to extend it with plugins and routes, how data flows from Kernel to your controllers, and why authentication lives here rather than in the kernel. Read this before building [plugins](/guides/gateway/plugins), auth flows, or aggregation routes on top of MDK. The Gateway is the backend layer of the [MDK App Toolkit](/concepts/stack/app-toolkit), which aligns the [plugin system](/guides/gateway/plugins) and [frontend packages](/concepts/stack/app-toolkit) into the supported development path for this monorepo. ## What the Gateway owns The Gateway wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) — the MDK protocol connector to Kernel — and adds an authenticated HTTP, WebSocket, and MCP interface on top. Consumers connect through the Gateway; using `@tetherto/mdk-client` without the Gateway is not supported by this monorepo. The Gateway owns three concerns that Kernel deliberately **does not** handle: - Authentication and RBAC: JWT validation, session management, OAuth2 (Google and Microsoft built in), and role-based access before any request reaches Kernel - API surface: REST endpoints, WebSocket telemetry subscriptions, command dispatch, and the MCP endpoint for AI agents - Fleet aggregation: cross-Worker queries that compute site hashrate, average temperature, and cross-rack efficiency — resolved in controller code, not in Kernel [Kernel](/concepts/stack/kernel) is a pass-through kernel. It routes commands to [Workers](/concepts/architecture#workers), collects telemetry, and maintains the device registry. Everything above the kernel — authentication, business logic, API surface — is owned by the caller: the Gateway (which wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) internally) when using the toolkit. ## Extension model The Gateway offers two ways to add routes, in order of preference. ### 1. Plugin system The recommended path. A plugin is a directory with an `mdk-plugin.json` manifest and one or more controller files. Pass the directory path to `startGateway()` via `extraPluginDirs`. Controllers receive a `services` bag on every request — `mdkClient`, `dataProxy`, `authLib`, and `conf` — with no protocol knowledge required. The [default plugins](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#default-plugins) (`auth`, `telemetry`, `site-hashrate`) load the same way as any plugin you write. The [plugin authoring guide](/guides/gateway/plugins) covers the build process end to end. The [plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) documents the manifest schema, controller contract, and loader errors. ### 2. Raw Fastify routes For one-off handlers that do not need a manifest, pass `additionalRoutes` to `startGateway()`. These are plain Fastify route objects — no `services` injection, no manifest validation, no auth wiring. Use this path sparingly; a plugin is easier to test in isolation and easier for a later maintainer to follow. ## Connect without the Gateway If your use case does not need the Gateway's HTTP surface, RBAC, or plugin system — for example, a background service that only dispatches commands — you can use [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) directly against Kernel without running the Gateway at all. This is the direct path. Such an approach is not directly supported by this monorepo, as most applications build on the Gateway. ## Data access Two services are available inside every plugin controller. **`services.mdkClient`** gives access to live Kernel data: pull a telemetry snapshot, dispatch a command, list registered Workers. It's `null` when the Gateway starts without a live Kernel connection, so guard it before use. **`services.dataProxy`** reads from persisted Worker tail-logs: time-series aggregation, historical hashrate, efficiency trends. Use this for data that does not require a live Kernel round-trip. The split exists because the two sources have different latency and availability characteristics. `mdkClient` calls are network operations that can fail if Kernel is unreachable. `dataProxy` reads from local storage and remains available whether Kernel is online or not. ## Authentication design The Gateway validates a JWT Bearer token before proxying any request to Kernel. By design, Kernel does not perform user-level authentication. The HRPC connection is an encrypted Noise channel, and [Kernel maintains an allowlist](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#transports); pre v1.0 it is opt-in (the default `auth.whitelist` is empty and admits any caller), but when configured the Gateway's DHT public key must be added before the connection is accepted. Once the transport is established, Kernel trusts all messages from the Gateway without inspecting user identity. User authentication and RBAC are entirely the Gateway's responsibility. RBAC is enforced at the route level via the `permissions` field in `mdk-plugin.json`. Routes with `"auth": false` are public — no JWT is required. Routes with `"auth": true` but no `permissions` array are accessible to any authenticated user. [A `permissions` array](/guides/gateway/plugins#auth-permissions-and-caching) restricts access further to users with matching roles. ## Kernel connection The Gateway is the **active** side of this connection — it dials Kernel. [Kernel](/concepts/stack/kernel) is the passive listener; it does not initiate contact with the Gateway. The connection is [Hyperswarm RPC (HRPC)](/reference/glossary#hyperswarm-rpc) — an encrypted peer-to-peer transport addressed by Kernel's public key. What varies is how the Gateway obtains that key: - **Same host (zero-config default)**: Kernel publishes its HRPC public key to a well-known key file (`/mdk/.kernel-key`) on start; the Gateway reads it from there automatically when no key is passed - **Separate hosts**: pass the key explicitly (`startGateway({ kernelKey })`), obtained from `kernel.getPublicKey()` on the Kernel host. When [Kernel's `auth.whitelist`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#transports) is configured, the Gateway's DHT public key must be added to it before the connection is accepted Pre v1.0, the allowlist is opt-in. Kernel's `auth.whitelist` defaults to empty, which admits any HRPC caller. When an allowlist is configured, the Gateway's DHT public key must appear in it before Kernel accepts the connection. ## Next steps - [Run the Gateway for the first time](/guides/gateway/run) - [Add routes with the plugin system](/guides/gateway/plugins) - Review the [full API and configuration reference](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - Choose a [deployment shape](/concepts/deployment-topologies) # Kernel (/concepts/stack/kernel) ## Overview [`@tetherto/mdk-kernel`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md) is the trusted coordination kernel at the heart of MDK. It routes commands, monitors device health, registers Workers, and pulls telemetry — without performing user authentication, business logic, or aggregation. Kernel is a pass-through kernel: it receives commands from any caller using [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) — most commonly the [Gateway](/concepts/stack/gateway) — and dispatches them to [Workers](/concepts/stack/workers); it pulls telemetry from Workers and routes it back to callers. Everything else is the caller's responsibility. ## What Kernel owns Kernel is decomposed into six single-responsibility modules. Modules communicate only through their declared interfaces. **`WorkerRegistry`**: maps `deviceId → workerId → RPC channel`. Source of truth for Worker-to-device routing. Workers progress through a state machine as Kernel discovers and registers them — Unregistered → Discovered → IdentitySaved → Ready → Terminated. **`CommandDispatcher`**: validates incoming command envelopes, resolves the owning Worker from the registry, checks that the command exists in the Worker's declared capabilities, then passes it to the state machine. Scope resolution (`COMMAND_SCOPES`: `device` | `worker` | `rack`) expands a single command to one or more target devices; a `MAX_TARGETS` cap (1024) is enforced before any state is written. **`CommandStateMachine`**: tracks every command's full execution lifecycle. Backed by a Write-Ahead Log (WAL) in Hyperbee — every state transition is persisted before it takes effect. On restart, `recover()` sweeps non-terminal states and retries or fails them — QUEUED → DISPATCHED → EXECUTING → SUCCESS (or FAILED / TIMEOUT). **`TelemetryCollector`**: stateless proxy. Routes `telemetry.pull` queries to the appropriate Worker and passes the response back to the caller. Workers own all aggregation and storage — Kernel is a thin router. **`Scheduler`**: system metronome. Runs non-overlapping interval jobs for telemetry pulls, health pings, and state pulls on configurable cadences. Jobs are idempotent — safe to restart with no state loss. **`HealthMonitor`**: ping-based liveness checker. Sends `health.ping` to every registered Worker on a configurable cadence and updates the registry — UNKNOWN → HEALTHY → SICK → DEAD (with reconnect path back to HEALTHY). ## The pull-only model Kernel never receives unsolicited data from Workers. It always initiates — pulling telemetry, pinging health, and pulling state on cadences set in `opts.cadences`. Workers become reachable and wait; Kernel reaches out on its own schedule. This is what prevents the kernel from being overwhelmed by upstream pressure and is why Workers are described as passive. Callers — typically the [Gateway](/concepts/stack/gateway#kernel-connection) — do send command requests to Kernel (Kernel is the receiver for those), but Kernel then dispatches each command to the owning Worker via its own initiated call. ## Transport Kernel is the **passive listener** — the [caller always initiates the connection](/concepts/stack/gateway#kernel-connection), over [Hyperswarm RPC (HRPC)](/reference/glossary#hyperswarm-rpc) — an encrypted peer-to-peer transport addressed by Kernel's public key. The [deployment topologies connection model](/concepts/deployment-topologies#connection-model) details the active/passive components. - **Same host (zero-config default)**: Kernel publishes its HRPC public key as hex to a well-known key file (`/mdk/.kernel-key`) on start; the caller/Gateway reads it from there automatically. The file is not deleted on shutdown — the key is stable across restarts because the HRPC seed persists in the kernel store - **Remote or multi-host**: the operator shares the key (`kernel.getPublicKey()`) with the caller/Gateway out-of-band. Kernel can maintain an allowlist — when configured, the caller/Gateway's DHT public key must be added to `opts.auth.whitelist` before the connection is accepted The [Kernel transport reference](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#transports) covers the allowlist key exchange and configuration options. ## What Kernel does not own Kernel deliberately excludes these concerns and delegates them to other layers: - **User authentication and RBAC**: JWT validation, session management, and role-based access are the Gateway's responsibility. Kernel trusts all messages from any established [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) connection without inspecting user identity. The Gateway is the only caller that enforces RBAC before reaching Kernel; using `@tetherto/mdk-client` without the Gateway carries no access control layer - **Business logic and aggregation**: cross-Worker queries, fleet statistics, and site-level aggregation belong in Gateway controllers, not in the kernel - **UI and consumer interfaces**: Kernel has no HTTP surface. Consumers connect through the Gateway's REST, WebSocket, or MCP endpoints ## Next steps - Understand the [Gateway's role as Kernel's consumer](/concepts/stack/gateway) - Understand [Workers as Kernel's downstream](/concepts/stack/workers) - Choose a [deployment shape](/concepts/deployment-topologies) - Start Kernel via the [`@tetherto/mdk` bootstrap API](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md) - Configure Kernel directly using the [`createKernel()` option surface](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md) # Workers (/concepts/stack/workers) ## Overview This page introduces the [Worker](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md) as a development component. It explains what a Worker owns, how Kernel discovers it, what the capability contract is, and how to build a Worker for new hardware. Read this before integrating new hardware, configuring discovery, or building on top of the Worker protocol. ## What a Worker owns A Worker wraps a device library and exposes it to Kernel via the MDK Protocol. Workers are the integration handlers between physical hardware and `@tetherto/mdk-kernel`, and the unyielding source of truth for that hardware. `@tetherto/mdk-kernel` operates purely as a synchronized state machine over Worker-reported state — it never reads hardware directly. Workers are **passive**: they become a reachable endpoint and wait. The Kernel initiates every call; Workers only ever respond. [Deployment topologies connection model](/concepts/deployment-topologies#connection-model) details how this directionality shapes transport choices. For [approval-gated writes](/concepts/control-plane#approval-gated-writes), Workers answer `write.calls.request` while the Kernel resolves candidate writes, then execute the approved write as a normal `command.request`. ## Discovery model How Kernel finds a Worker depends on whether they share a machine. Each Worker package supplies its own boot function that constructs [`WorkerRuntime`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime.js) internally (for example `startWhatsminerWorker`, or `startVendorWorker` if you're [building your own](/guides/workers/build-a-worker)) — there is no single generic `startWorker(WorkerClass, opts)` entry point. The code samples below use `startYourWorker` as a stand-in for whichever boot function your Worker package exports. In all cases, the post-discovery sequence is identical — [Kernel requests identity, registers the Worker, then queries its capabilities](/concepts/stack/kernel#what-kernel-owns). | Mode | How Kernel finds the Worker | When to use | | --- | --- | --- | | **DHT** | The Worker's host process passes `kernelTopic` to `WorkerRuntime`; Kernel listens on the same topic and connects automatically | [Production microservices](/guides/deployment/run-microservices-site), Workers on separate hosts or networks | | **Local** | The Worker's host process publishes the runtime's RPC key to a shared directory; Kernel watches the directory: no DHT needed | All components on one machine, restricted outbound networking | | **Same-process** | The Worker's host process calls `kernel.registerWorker(runtime.getPublicKey())` directly: no network lookup | [Getting started](/tutorials/backend-stack/run), [single-process sites](/guides/deployment/run-single-process-site) | Discovery is a startup concern only — it determines how Kernel obtains the Worker's RPC public key, nothing more. Once connected, all three modes use the same HyperswarmRPC transport and the same MDK Protocol envelope (`command.request`, `telemetry.pull`, and so on). Local and same-process modes route traffic over the local network interface; DHT mode routes over the public internet. The available commands, telemetry, and operations are identical in all three. After a Worker reaches `READY`, the [Kernel Scheduler](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#scheduler) initiates telemetry pulls and health checks over HRPC; the Worker remains passive. ### DHT mode In multi-process DHT mode, Kernel and the Worker must join the same Hyperswarm topic. Generate a random 32-byte hex topic in whichever process starts first, persist it somewhere the other process can read it, and pass the same value to both sides: ```js const kernel = await getKernel({ topic: '<32-byte-hex>' }) const worker = await startYourWorker({ kernelTopic: '<32-byte-hex>', ...opts }) ``` The Worker must join the topic before Kernel starts listening. Start the Worker process first, then start Kernel. `waitForDiscovery()` polls the registry until discovered Workers reach `READY` state. The DHT pattern is demonstrated end-to-end in [`dht-worker.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-worker.js) and [`dht-kernel.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-kernel.js). ### Local mode In local mode, Kernel and Workers coordinate through a shared directory on the same machine (default `/.worker-keys/`). No Hyperswarm topic is joined and no outbound internet connection is required. **Worker side**: after `runtime.start()`, publish the runtime's RPC key to the shared directory with `publishWorkerKey` from `@tetherto/mdk`'s local-discovery helpers. The entry is stable across restarts (the key is seed-derived), so restarting a Worker is a no-op from Kernel's perspective. ```js const { keysDir, publishWorkerKey } = require('@tetherto/mdk/backend/core/mdk/lib/local-discovery') const worker = await startYourWorker(opts) publishWorkerKey(keysDir(root), workerId, worker.runtime.getPublicKey().toString('hex')) ``` **Kernel side**: `getKernel` watches the directory with `fs.watch` and runs a full scan every four seconds. Each entry found triggers the normal discovery listener (Identity → Capability → Ready), the same sequence used in DHT mode. ```js const kernel = await getKernel({ discovery: { mode: 'local' } }) ``` A custom directory can be passed when the default path is not suitable: ```js const kernel = await getKernel({ discovery: { mode: 'local', dir: '/shared/mdk-keys' } }) publishWorkerKey('/shared/mdk-keys', workerId, worker.runtime.getPublicKey().toString('hex')) ``` Keys persist across restarts and the directory is read again each time Kernel starts, so Workers and Kernel can start in any order without coordination. All processes must share the same filesystem path. Local mode requires every component to run on the same machine — use DHT mode for Workers on separate hosts. The [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md#how-out-of-process-workers-find-the-kernel) demonstrates local mode as its default multi-process setup — `up --discovery local` starts all Workers in local mode, and `up --discovery dht` switches to DHT without any other code change. ### Same-process mode Same-process mode skips all network discovery. Register the runtime's public key directly with the live Kernel instance — no topic, no directory, no network lookup: ```js const kernel = await getKernel(opts) const worker = await startYourWorker(opts) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Two behaviors differ from DHT and local mode: - **Registration**: the host module calls `kernel.registerWorker()` directly with the runtime's public key. The Worker reaches `READY` synchronously — no `waitForDiscovery()` required - **Lifecycle**: registration alone does not couple the Worker's shutdown to Kernel's — the host process that constructed `WorkerRuntime` owns its lifecycle in every mode. Push the Worker's `stop()` onto Kernel's `_cleanup` queue yourself if Kernel shutdown should cascade to it (see [`bootWorker`](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md#how-out-of-process-workers-find-the-kernel) for the pattern), or manage it directly in your own shutdown handler Use the same-process mode for the [get-started tutorial](/tutorials/backend-stack/run) and [single-process deployments](/guides/deployment/run-single-process-site). For multi-process, use DHT or local mode instead. ## Capability contract [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/plugin/mdk-contract.json) is the canonical source of truth for a Worker's programmatic capabilities **and** its AI context. MDK deliberately merges formal validation and semantic guidance into a single JSON contract: - `description` does double duty as the human UI label and AI edge-case rule (for example, *"Outlet temperature > 85C requires intervention"*) - `constraints` governs orchestration limits - `troubleshooting` provides if/then recovery behaviors alongside the payload it evaluates The exhaustive JSON Schema is `mdk-contract.schema.json`, with a [reference instance at `mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/plugin/mdk-contract.json). ## Add hardware External integrators add new hardware by building a Worker Plugin that conforms to the strict Device-Lib Contract: 1. Reference [`mdk-contract.schema.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) to author the [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/plugin/mdk-contract.json), validating strict data schemas while injecting explanations, constraints, and troubleshooting directly into the relevant nodes. 2. Build a Worker Plugin: the object `{ contract, dir, connect, disconnect? }`, where `connect`/`disconnect` translate `command.request` and `telemetry.pull` calls into device I/O. Pass it to `new WorkerRuntime(plugin, opts)` from [`@tetherto/mdk-worker`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime.js) — `WorkerRuntime` hosts every device behind one HRPC channel to Kernel, invoking each handler as `(ctx, params)` and wrapping the result in the MDK Protocol envelope itself, so handlers never see transport. 3. Boot the Worker instance, connect to devices, and register with `@tetherto/mdk-kernel` using the appropriate [discovery mode](#discovery-model). `@tetherto/mdk-kernel` detects the peer and pulls its identity and capabilities.
Migrating from MDKWorkerAdapter / ThingManager (pre-0.5.0) `WorkerRuntime` generalizes the former `MDKWorkerAdapter` (persistent seeds, single HRPC respond loop, DHT topic announce carried over) and replaces `ThingManager` delegation with per-device handler dispatch. See [Worker Runtime legacy services](https://github.com/tetherto/mdk/blob/main/docs/reference/maintainers/worker-runtime-legacy-services.md) for the full migration history and the optional built-in services surface.
See the [full build walkthrough](/guides/workers/build-a-worker) for a step-by-step guide, or [`whatsminer/plugin/index.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/plugin/index.js) for a reference plugin implementing `connect`/`disconnect` against a real device. ## Next steps - [Configure how often Kernel polls discovered Workers](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#api) - [Diagnose startup hangs when outbound network is restricted](/guides/miners/troubleshooting) - Read the [full build walkthrough](/guides/workers/build-a-worker) for a step-by-step guide to building a new Worker Plugin # Guides (/guides) } title="Run an MDK site" href="/guides/deployment" description="Choose a deployment topology and run a production or single-process MDK site" /> } title="Gateway" href="/guides/gateway" description="Run, configure, and extend the MDK Gateway" /> } title="Miner Workers" href="/guides/miners" description="Connect Antminer, Avalon, or Whatsminer hardware to an MDK stack" /> } title="UI" href="/guides/ui" description="Compose reporting layouts and wire the React UI Devkit into your app" /> # Run an MDK site (/guides/deployment) ## Overview Use these guides to choose a site deployment shape. If Kernel, Gateway, Worker, manager, or thing are unfamiliar, read [terminology](/reference/glossary) first. If you are choosing between topologies, read [deployment topologies](/concepts/deployment-topologies). ## Choose a guide - [Single-process](/guides/deployment/run-single-process-site) — run Kernel, Gateway, and Workers in one Node.js process - [Local](/guides/deployment/run-all-workers-site) — run the supported Worker fleet as separate processes on one machine, each with registered devices - [Microservices](/guides/deployment/run-microservices-site) — run Gateway and Workers as separate supervised services ## Next steps - Understand the trade-offs before you choose your [deployment topology](/concepts/deployment-topologies) - Browse the [functions](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md) that wire together the [Kernel](/concepts/stack/kernel), [device Workers](/concepts/stack/workers), and the [Gateway](/concepts/stack/gateway) HTTP # Run the supported Worker fleet with mock devices (/guides/deployment/run-all-workers-site) This page directs you to the correct location for the prerequisites, run command, smoke test, and troubleshooting. ## Overview Use this example when you want the to run a demo for multiple configured Workers across the device families - for miners, containers, power meters, sensors, and pools - running as separate processes. Each talks to mock hardware that speaks the real wire protocol. The site Gateway plugin surfaces all device data through a single `/site` HTTP API. This example runs a microservices topology. Use this when: - You want to explore the supported Worker fleet and its telemetry in one running system - You are testing PM2 or Docker orchestration before deploying to hardware - You want real driver code running its full connect, collect, and command paths (only the endpoints are localhost mocks instead of hardware) - You want the site Gateway plugin as a starting point for your own `/site` API You have a choice of [deployment topologies](/concepts/deployment-topologies) from microservices to single-process. ## Run the example Follow the [site example](https://github.com/tetherto/mdk/tree/main/examples/site-backend): - Start with the [prerequisites](https://github.com/tetherto/mdk/tree/main/examples/site-backend#prerequisites) - Choose your launch method: - Use [PM2](https://github.com/tetherto/mdk/tree/main/examples/site-backend#run-with-pm2-one-host-multiple-processes) for local process supervision on one host - Use [Docker](https://github.com/tetherto/mdk/tree/main/examples/site-backend#run-with-docker-one-container-per-process) when you want containerized services or Compose-managed startup - [Verify](https://github.com/tetherto/mdk/tree/main/examples/site-backend#verify) the fleet is up with a single `curl` ## Next steps - Understand the trade-offs between [deployment topologies](/concepts/deployment-topologies) - Run [a single-process site](/guides/deployment/run-single-process-site) for the simpler local topology - Extend the Gateway HTTP API with [custom plugins](/guides/gateway/plugins) - Browse the [functions](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md) that wire together the [Kernel](/concepts/stack/kernel), [device Workers](/concepts/stack/workers), and the [Gateway](/concepts/stack/gateway) HTTP - Build your own Worker from scratch # Run a microservices site (/guides/deployment/run-microservices-site) This thin page directs you to the correct location for the prerequisites, config fields, run command, smoke test, and troubleshooting. ## Overview Use the **microservices** site example when you want the Gateway and Workers to run as separate OS processes or containers. This page is the task guide for the microservices topology. The [deployment topologies](/concepts/deployment-topologies) concept explains when to choose microservices instead of single-process. ## Use this topology when - You need supervisor-managed restarts and logs - You want to restart or scale one service without restarting the others - You want a production-like layout for Gateway and Workers ## Run the example The `examples/backend/site/` example starts only the Gateway and Worker processes — Kernel must run separately. For a self-contained example that starts every service including Kernel, use [`examples/site-backend/`](https://github.com/tetherto/mdk/tree/main/examples/site-backend) instead. Follow the [microservices site example](https://github.com/tetherto/mdk/tree/main/examples/backend/site): - Start with the [prerequisites](https://github.com/tetherto/mdk/tree/main/examples/backend/site#prerequisites) - Use the [PM2 steps](https://github.com/tetherto/mdk/tree/main/examples/backend/site#pm2) for local process supervision on one host - Use the [Docker steps](https://github.com/tetherto/mdk/tree/main/examples/backend/site#docker) when you want containerized services or Compose-managed startup ## Next steps - Compare the supported shapes: [Deployment topologies](/concepts/deployment-topologies) - Run the simpler local topology — [Run a single-process site](/guides/deployment/run-single-process-site) - Register a single miner before building a site config — [Run a miner Worker](/guides/miners) # Run a single-process site (/guides/deployment/run-single-process-site) This thin page directs you to the correct location for the prerequisites, config fields, run command, smoke test, and troubleshooting. ## Overview Use the **single-process** site example when you want Kernel, the Gateway, and Worker to share one Node.js process. This page is the task guide for the single-process topology. The [deployment topologies](/concepts/deployment-topologies) concept explains when to choose single-process instead of microservices. ## Use this topology when - You are developing locally, running demos, or writing self-contained tests - You want a minimal-footprint deployment - You do not need per-service restart isolation ## Run the example Follow the [single-process site example](https://github.com/tetherto/mdk/tree/main/examples/backend/site-single-process): - Start with its [prerequisites](https://github.com/tetherto/mdk/tree/main/examples/backend/site-single-process#prerequisites) - Use the example [quickstart](https://github.com/tetherto/mdk/tree/main/examples/backend/site-single-process#quickstart) ## Next steps - Compare the supported shapes: [Deployment topologies](/concepts/deployment-topologies) - Run the supervised topology — [Run a microservices site](/guides/deployment/run-microservices-site) - Register a single miner before building a site config — [Run a miner Worker](/guides/miners) # Gateway how-to guides (/guides/gateway) ## Overview The Gateway wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) to deliver an authenticated HTTP, WebSocket, and MCP interface for your frontend and AI agents. These guides cover how to run it and extend it with the plugin system. If Gateway, Kernel, or plugin are unfamiliar, read [terminology](/reference/glossary) first. For the full developer model — extension, data access, auth design — read the [Gateway concept page](/concepts/stack/gateway). ## Choose a guide | Goal | Guide | | --- | --- | | Start the Gateway for the first time | [Run the Gateway](/guides/gateway/run) | | Use built-in plugins or build your own | [Gateway plugins](/guides/gateway/plugins) | | Stop Kernel, Gateway, and Workers cleanly | [Tear down MDK services](/guides/gateway/teardown) | | Operator in the loop: submit and approve write actions | [Submit and approve write actions](/guides/gateway/write-actions) | ## Next steps - [Understand the Gateway as a development surface](/concepts/stack/gateway) - Read the [Gateway API reference](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - Choose a [deployment shape](/concepts/deployment-topologies) # Gateway plugins (/guides/gateway/plugins) ## Overview The Gateway exposes HTTP routes through a declarative plugin system. Each plugin is a directory containing an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) manifest and one or more controller files. MDK ships a set of default plugins that load automatically; you can mount additional plugins for your own site logic. Plugins call into the Kernel through `services.mdkClient`, an instance of [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md). No knowledge of the MDK Protocol envelope or internal message shapes is required. ## Default plugins MDK ships plugins that load automatically on Gateway startup: - The `auth` plugin serves identity and token endpoints under `/auth` - The `telemetry` plugin serves site metrics (hashrate, consumption, efficiency, temperature, and more) under `/auth/metrics` - The `site-hashrate` plugin serves aggregated site hashrate history The [plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) lists every default route, its method, and whether it needs a token — those tables are generated from each plugin's `mdk-plugin.json`. Plugins you mount yourself are documented by their own manifests. ### Mount a plugin Pass an `extraPluginDirs` array to `startGateway()` to load additional plugins at boot alongside the default plugins: ```js const { startGateway } = require('@tetherto/mdk') await startGateway({ kernel, port: 3000, extraPluginDirs: [ path.join(__dirname, 'plugins/custom-metrics'), path.join(__dirname, 'plugins/alerts') ] }) ``` Each entry must be an absolute path to a directory containing an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format). The plugin loader validates the manifest and all handler files at startup — missing files or invalid manifests throw immediately before the server comes up. ### Build a plugin A plugin is a directory with two things: a manifest and controllers. #### 1.1 Create the manifest [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) declares the plugin identity (`name`, `version`) and a `routes` array. Each route needs an `id`, a `handler` path, and an `http` block with a `method` and `path`. Rather than copy a synthetic example, start from a real manifest and trim it: - [`examples/full-site/plugins/site/mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/examples/full-site/plugins/site/mdk-plugin.json) — three routes including a `GET`, a `POST` with a `requestBody`, and path parameters - [`backend/core/plugins/telemetry/mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/telemetry/mdk-plugin.json) — auth, caching, query parameters, and named-export handlers Path parameters use `{param}` syntax — the loader normalises them to Fastify's `:param` format. For named exports use `"handler": "./controllers/foo.js#namedExport"`. The [plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) explains what each field means and what the loader requires. #### 1.2 Write a controller Every controller exports an `async function (req, services)`: ```js // controllers/live.js — read live telemetry module.exports = async function live (req, services) { const deviceId = req.query.deviceId const telemetry = await services.mdkClient.pullTelemetry(deviceId, 'metrics') return { deviceId, ...telemetry } } ``` ```js // controllers/command.js — dispatch a command module.exports = async function command (req, services) { const deviceId = req.params.deviceId const { mode } = req.body const result = await services.mdkClient.sendCommand(deviceId, 'setPowerMode', { mode }) return { deviceId, commandId: result.commandId, status: result.status } } ``` ### The `req` object | Field | Type | Contains | | --- | --- | --- | | `req.params` | `object` | Path parameters (e.g. `{ deviceId: 'wm-001' }`) | | `req.query` | `object` | Query string parameters | | `req.body` | `object` | Parsed JSON request body | | `req.headers` | `object` | HTTP headers | | `req._info` | `object` | Internal request metadata (rarely needed) | ### The `services` object | Field | Type | Use for | | --- | --- | --- | | `services.mdkClient` | `MdkClient` | Live reads and command dispatch — `sendCommand`, `pullTelemetry`, `getCapabilities`, `listWorkers` | | `services.dataProxy` | `DataProxy` | Historical and aggregated data from Worker tail-logs — `requestData`, `requestDataMap` | | `services.authLib` | `AuthLib` | JWT and session helpers (needed only for advanced auth flows) | | `services.conf` | `object` | Gateway runtime config | Always guard `services.mdkClient` — it is `null` when the Gateway starts without a live Kernel connection: ```js if (!services.mdkClient) throw new Error('ERR_MDK_CLIENT_UNAVAILABLE') ``` Always guard `services.authLib` — it is `undefined` when the Gateway starts with `noAuth: true`. Any call to `authLib.tokenHasPerms()` or `authLib.getTokenPerms()` throws `Cannot read properties of undefined` in `noAuth` mode: ```js if (!services.authLib) throw new Error('ERR_AUTH_LIB_UNAVAILABLE') ``` ### Read hardware data For live device data use `mdkClient`: ```js // Pull a live metrics snapshot const tel = await services.mdkClient.pullTelemetry(deviceId, 'metrics') // Pull the declared capabilities (from the Worker's mdk-contract.json) const { capabilities } = await services.mdkClient.getCapabilities(deviceId) // List all registered Workers const { workers } = await services.mdkClient.listWorkers() ``` For historical or aggregated series from a Worker's persisted tail-log use `dataProxy`: ```js const results = await services.dataProxy.requestData('tailLogRangeAggr', { type: 'miner', startDate: start, endDate: end, fields: { hashrate_sum: 1 } }) ``` The [default telemetry controllers](https://github.com/tetherto/mdk/tree/main/backend/core/plugins/telemetry/controllers) show worked examples of both patterns. ### Send a command `sendCommand` dispatches via the Kernel to the Worker that owns the device. The command must be declared in the Worker's `mdk-contract.json`. It returns: | Field | Type | Description | | --- | --- | --- | | `commandId` | `string` | Correlation ID generated by Kernel. Echo this to the HTTP caller so they can track the operation. | | `status` | `string` | `'SUCCESS'` or `'FAILED'` | | `result` | `object` | Command-specific response payload (present when status is `'SUCCESS'`) | | `error` | `string` | Error message (present when status is `'FAILED'`) | ```js const result = await services.mdkClient.sendCommand(deviceId, 'reboot', {}) if (result.status === 'FAILED') throw new Error(result.error) return { commandId: result.commandId, status: result.status } ``` ### Auth, permissions, and caching **Auth** — set `"auth": true` on a route to require a valid Bearer token. The adapter runs `authCheck` before the handler is called. **Permissions** — add a `"permissions"` array to enforce RBAC: ```json { "id": "site.miners.command", "auth": true, "permissions": ["actions:w"], "handler": "./controllers/command.js", "http": { "method": "POST", "path": "/site/miners/{deviceId}/command" } } ``` **Caching** — add a `"cache"` array of dot-path strings to enable request-level caching. The cache key is composed from the route ID and the resolved values of each path: ```json { "id": "telemetry.hashrate", "cache": ["query.start", "query.end", "query.groupBy"], ... } ``` Pass `?overwriteCache=true` to any cached route to bypass and refresh. ### Manifest validation errors The plugin loader validates every manifest and handler at startup and throws if anything is wrong: | Error | Cause | | --- | --- | | `ERR_PLUGIN_MANIFEST_MISSING` | No `mdk-plugin.json` found in the plugin directory | | `ERR_PLUGIN_MANIFEST_INVALID` | JSON parse error, or missing required field (`name`, `version`, or `routes`) | | `ERR_PLUGIN_ROUTE_DUPLICATE_ID` | Two routes in the same manifest share the same `id` | | `ERR_PLUGIN_HANDLER_NOT_FOUND` | The `handler` file path does not exist or failed to load | | `ERR_PLUGIN_HANDLER_NOT_FUNCTION` | The handler file exports something other than a function | ## Troubleshooting
Migrate from v0.2 to v0.3
In v0.3, `metricsRoutes` and `devicesRoutes` were removed from `backend/core/gateway/workers/lib/server/index.js`. The auth and telemetry endpoints they registered are now delivered by the default plugins, which load automatically — no action needed for those. If your v0.2 code patched or monkey-patched those registrations to inject custom logic: 1. Create a plugin directory with an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) and controller files for the routes you were injecting. 2. Pass the directory to `startGateway()` via `extraPluginDirs`. ```js // Before (v0.2 — no longer works) const server = require('@tetherto/mdk-gateway/workers/lib/server') server.metricsRoutes.push(myCustomRoute) // After (v0.3+) await startGateway({ kernel, extraPluginDirs: [path.join(__dirname, 'plugins/my-metrics')] }) ```
## Next steps - Try the [live site backend example](/guides/deployment/run-all-workers-site) for a complete worked plugin with three routes: a live site overview, a historical series, and a command endpoint running under PM2 or Docker - Build the minimal dashboard tutorial — end-to-end worked example of the single-plugin + controller pattern - Understand how Workers declare their data via `mdk-contract.json` — what `mdkClient` reads and `sendCommand` dispatches - See the full [manifest and services reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) - Review the [Gateway API and config](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) # Run the Gateway (/guides/gateway/run) ## Overview This guide covers three ways to run the Gateway: programmatically via `startGateway()` (the standard production path), connected to a remote Kernel over HRPC (cross-host deployments), and as a standalone process from the source tree (for contributors). If Gateway, Kernel, or plugin are unfamiliar, read [terminology](/reference/glossary) first. For a deeper explanation of what the Gateway owns and how it connects to Kernel, read the [Gateway concept page](/concepts/stack/gateway). ## Prerequisites - Node.js >=24 (LTS) - npm >=11 - Commands are run from the repository root - An Kernel instance running and reachable, or `kernelKey: false` to start without a Kernel connection (development only) ### Programmatic path Most teams embed `startGateway()` in their own Node.js application rather than running the Gateway as a separate process. This is the standard production path. #### 1.1 Development (no auth) Use `noAuth: true` during local development to skip the JWT requirement: ```js const { getKernel, startGateway } = require('@tetherto/mdk') const kernel = await getKernel() const server = await startGateway({ kernel, port: 3000, noAuth: true }) // HTTP server is up at http://localhost:3000 ``` `noAuth: true` disables JWT validation on all routes. Never use this in production. #### 1.2 Production (OAuth2) Pass an `auth` block to enable OAuth2. Google and Microsoft providers are built in: ```js const { getKernel, startGateway } = require('@tetherto/mdk') const kernel = await getKernel() const server = await startGateway({ kernel, port: 3000, auth: { h0: { method: 'google', credentials: { client: { id: 'YOUR_CLIENT_ID', secret: 'YOUR_CLIENT_SECRET' } }, users: ['admin@example.com'] } } }) ``` Replace the `users` array with the email addresses that should have access. A copy of the full OAuth2 config format ships in [`backend/core/gateway/config/facs/httpd-oauth2.config.json.example`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/config/facs/httpd-oauth2.config.json.example). The generated `httpd-oauth2.config.json` (written to `opts.root/config/facs/` on first start) persists your settings across restarts — edit that file rather than the code. The full configuration reference, including all `startGateway()` options, is in the [Gateway API reference](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md). ### Cross-host path (HRPC) Use this path when Kernel runs on a separate host. Pass the Kernel HRPC listener public key to `startGateway()` instead of an Kernel instance. (On a single host, neither is needed: `startGateway()` reads the key from the well-known key file that `getKernel()` publishes — see the [key resolution order](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md).) #### 2.1 Obtain the Kernel listener key On the host running Kernel, start Kernel and print its public key: ```js const { getKernel } = require('@tetherto/mdk') const kernel = await getKernel() console.log('Kernel listener key:', kernel.getPublicKey().toString('hex')) ``` Share that hex string with the Gateway host. #### 2.2 Start the Gateway with `kernelKey` ```js const { startGateway } = require('@tetherto/mdk') const server = await startGateway({ kernelKey: '', port: 3000, noAuth: true // replace with auth config for production }) ``` Pre v1.0, Kernel's `auth.whitelist` defaults to empty and admits any HRPC caller. For production deployments, add the Gateway's DHT public key to Kernel's allowlist — see the [Gateway concept page](/concepts/stack/gateway) and [`opts.kernelKey` reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md). ### Standalone path To run the Gateway directly from the source tree without embedding it: ```bash cd backend/core/gateway npm install npm run dev ``` For production mode: ```bash npm start ``` The standalone path is intended for contributors working on the Gateway itself. For application development, embed `startGateway()` in your own project rather than running it standalone. ## Next steps - [Add routes with the plugin system](/guides/gateway/plugins) - [Review all configuration options](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - Understand the [extension model, auth design, and Kernel connection](/concepts/stack/gateway) - Choose a [deployment shape](/concepts/deployment-topologies) # Tear down MDK services (/guides/gateway/teardown) ## Overview MDK registers graceful shutdown handlers automatically when you start services with `getKernel()`, `startWorker()`, or `startGateway()`. For most deployments, `SIGINT` (Ctrl+C) triggers a clean teardown with no extra code. This guide covers the three situations where you need to think about teardown explicitly: - [Automatic teardown](#automatic-teardown-with-getkernel) - [Explicit teardown](#explicit-teardown-in-tests-or-scripted-runs) - [Custom signal handling](#custom-signal-handling-with-onshutdown) ## Prerequisites - Familiarity with the [Gateway](/concepts/stack/gateway) - MDK [installed and a working boot sequence](/guides/gateway/run) ### Automatic teardown with `getKernel()` `getKernel()` registers `SIGINT`/`SIGTERM` handlers internally. Any Workers or Gateway instances started with `opts.kernel` are chained into the cleanup sequence automatically — no extra code needed. ```js const { getKernel, startWorker, startGateway } = require('@tetherto/mdk') const { WM_M56S } = require('@tetherto/mdk-worker-whatsminer') const kernel = await getKernel() const { manager } = await startWorker(WM_M56S, { kernel }) await startGateway({ kernel, port: 3000, noAuth: true }) // Press Ctrl+C — MDK stops Gateway, Worker, then Kernel automatically. ``` See [`getKernel` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#getkernelopts--promisekernelmanager). ### Explicit teardown in tests or scripted runs Short-lived processes — integration tests, one-shot scripts — never receive `SIGINT`. Call `shutdown(kernel)` directly to drain the full cleanup chain. Pass the `kernel` object returned by `getKernel()`; passing a server object stops only the Gateway. ```js const { getKernel, startGateway, shutdown } = require('@tetherto/mdk') const kernel = await getKernel() await startGateway({ kernel, noAuth: true }) // … run assertions or perform work … await shutdown(kernel) // stops Gateway (chained), then stops Kernel ``` See [`shutdown` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#shutdownhandle--promisevoid). ### Custom signal handling with `onShutdown` Use `onShutdown` when you need to close resources outside an MDK boot object — for example, a database connection or a log buffer. ```js const { onShutdown } = require('@tetherto/mdk') onShutdown(async () => { await db.close() await logger.flush() }, { forceMs: 5000 }) ``` See [`onShutdown` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#onshutdowncleanupfn-opts--handler). ## What just happened 1. **Automatic chain**: `getKernel()`, `startWorker({ kernel })`, and `startGateway({ kernel })` wire themselves into `kernel._cleanup` so a single signal stops everything in order. 2. **Explicit drain**: `shutdown(kernel)` gives you the same ordered teardown on demand, without a signal. 3. **Custom hooks**: `onShutdown(fn)` lets you attach cleanup logic outside the MDK object hierarchy. ## Next steps - Full API reference — [`@tetherto/mdk` README](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md) - [Run the Gateway](/guides/gateway/run) # Write actions (/guides/gateway/write-actions) ## Overview This guide demonstrates how to submit approval-gated write actions from a React app, review the server-side voting queue, and approve, reject, or cancel pending actions through the Gateway. ## Prerequisites - The [Gateway is running](/guides/gateway/run) with auth enabled - The signed-in user has the Gateway [`actions:w` permission](/guides/gateway/plugins#auth-permissions-and-caching) - The signed-in user also has the target device-family write permissions required by the action, such as `miner:w` or `container:w` - The React app is wrapped in [``](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md#surface) - The feature stages write actions in [`actionsStore`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md#write-action-hooks) from `@tetherto/mdk-ui-foundation` or provides actions through an existing feature such as [Pool Manager](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/blueprints/pool-manager.md) ### Submit staged actions #### 1.1 Submit a single action Use `useSubmitSingleAction()` when the UI lets an operator submit one staged action by id. ```tsx function SubmitActionButton({ actionId }: { actionId: number }) { const submit = useSubmitSingleAction(); return ( ); } ``` #### 1.2 Submit all staged actions Use `useSubmitPendingActions()` when the UI has a review tray or bulk-submit control that should send the whole local staging queue. ```tsx function SubmitActionsButton() { const submitPending = useSubmitPendingActions(); return ( ); } ``` ### Review the server-side queue After submission, actions move from the local staging queue into the Gateway `/auth/actions*` voting surface. #### 2.1 Review with `usePendingActions()` Use `usePendingActions()` for a pending-action review table. Pass `refetchInterval` to override the default poll cadence (see [hook reference](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md#write-action-hooks)). ```tsx function PendingActionsList() { const { data: pending = [], isLoading } = usePendingActions({ refetchInterval: 5000, }); if (isLoading) return

Loading pending actions...

; return (
    {pending.map((action) => (
  • {action.id}
  • ))}
); } ``` #### 2.2 Review with `useLiveActions()` Use `useLiveActions()` when the UI needs to separate the current user's actions from others and gate approve/reject controls on `canApprove`. For polling cadence and role logic, see the [hook reference](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md#write-action-hooks).
### Approve or reject an action Use `useVoteOnAction()` to cast an approval or rejection. The hook calls `PUT /auth/actions/voting/:id/vote` and invalidates the relevant action caches. Disable direct vote buttons when `canVote` is false. Review-tray UIs that approve other users' actions should combine this mutation with `useLiveActions().canApprove`. ```tsx function VoteButtons({ actionId }: { actionId: string }) { const vote = useVoteOnAction(); return ( <> ); } ``` ### Cancel pending actions Use `useCancelAction()` when the current operator should withdraw one or more pending actions before the vote thresholds are met. The Gateway exposes the voting cancel route at `DELETE /auth/actions/voting/cancel`. ```tsx function CancelActionButton({ actionId }: { actionId: string }) { const cancel = useCancelAction(); return ( ); } ``` ### Verify the result Approved actions become command requests after the configured vote thresholds are met. Watch the feature state that initiated the action, or poll the action list with `usePendingActions()` / `useLiveActions()` until the item leaves the voting queue. For Pool Manager screens, use the existing [actions sidebar USAGE](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-manager/actions-sidebar/USAGE.md) and [Pool Manager blueprint](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/blueprints/pool-manager.md) as the integration examples.
## Next steps - Understand the [approval-gated write architecture](/concepts/control-plane#approval-gated-writes) — including how approved actions become normal command requests - Understand [plugin permission syntax, auth, permissions, and caching](/guides/gateway/plugins#auth-permissions-and-caching) - Configure route permissions in [Gateway plugins](/guides/gateway/plugins) - Review hook exports in [`@tetherto/mdk-react-adapter`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md) - Run integration coverage: [`backend/core/kernel/tests/integration/actions.test.js`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/tests/integration/actions.test.js) # Run a miner Worker (/guides/miners) ## Overview MDK drives each miner brand through its own Worker. These guides are task-focused and **independent** — you only need the one for the hardware you operate. If Kernel, Worker, manager, or thing are unfamiliar, read [terminology](/reference/glossary) first. ## Pick your hardware The authoritative model list for every Worker is the generated [supported-hardware catalogue](/reference/supported-hardware#miners). For example, you may: - [Run an Antminer Worker](/guides/miners/run-antminer-worker) - [Run a Whatsminer Worker](/guides/miners/run-whatsminer-worker) - [Run an Avalon Worker](/guides/miners/run-avalon-worker) ## Prerequisites Every guide assumes: - Node.js >=24 (LTS) - npm >=11 - Commands are run from the repo root - Outbound network access for Kernel discovery For the mock/development path: - No physical miner is required - The runnable example for your model starts the bundled mock device and registers it HRPC relies on HyperDHT for peer connectivity. Use the [network requirements and checks](/guides/miners/troubleshooting) if an example stalls before printing the Kernel key. For the deployment path: - A Node.js service or script in your deployment that runs the MDK Worker and registers devices - A supported miner reachable from the machine or container running the Worker - Access to the miner's native API and credentials, if that API requires them - The Worker's `USAGE.md` for the exact `registerThing` options ## Next steps - Browse [supported hardware](/reference/supported-hardware) - New to the moving parts? Read [terminology](/reference/glossary) (Kernel, Worker, manager, thing, mock) - If an example does not start or a mock port is busy, use [troubleshooting](/guides/miners/troubleshooting) - Drive the registered device from the CLI or dashboard: [Get started](/tutorials/backend-stack) # Run an Antminer Worker (/guides/miners/run-antminer-worker) ## Overview This page details how to run the Bitmain Antminer Worker. Select the development (mock) or real-device path. ## Prerequisites Review the [common deployment prerequisites](/guides/miners#prerequisites) before you start. Deployment-specific requirements: - A Node.js service or script in your deployment that runs the MDK Worker and registers devices - A supported Antminer device reachable from the machine or container running the Worker - The miner API reachable over HTTP, typically port `80` - Digest-auth credentials for the miner. Antminer devices commonly default to username `root` and password `root`, but use your site's configured credentials ### Development
Run against a mock To support development, this repo ships a config-driven runnable example that boots a mock device per configured Worker, starts a Kernel and Gateway, and starts each Worker (`startAntminerWorker`) against its mock: ```bash node examples/backend/miners/antminer/index.js ``` It falls back to the committed example config (`config/mdk.config.json.example`) when no local `config/mdk.config.json` is present, so it runs clone-and-run with zero setup. It prints the Kernel HRPC key and one line per registered device, then stays running until Ctrl+C. For details on the boot options and mock, see [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md).
### Connect a miner #### 2.1 Pick your model Use the Antminer Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md) to confirm the `model` value and mock `type` for your device. This guide uses `s21`; replace it with the value for your miner. #### 2.2 Register your miner Antminer devices use an HTTP API with digest authentication. Add this code to the Node.js service or script that runs the MDK Worker in your deployment. The snippet shows the minimum boot call seeding one Antminer device; replace the example IP address and credentials with your miner's values: ```js const { getKernel } = require('@tetherto/mdk') const { startAntminerWorker } = require('@tetherto/mdk-worker-antminer') const kernel = await getKernel() const worker = await startAntminerWorker({ workerId: 'antminer-rack-1', model: 's21', storeDir: './store/antminer-rack-1', seedDevices: [{ info: { container: 'site-1', serialNum: 'AM-001' }, opts: { address: '192.168.1.20', port: 80, username: 'root', password: 'root' } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Make sure each miner's IP is reachable from the machine or container running the Worker before registering. Commands act on physical hardware — prioritize thermal safety. `seedDevices` only seeds a fresh, empty `storeDir` — once persisted, the device set survives restarts on its own. To add a device to an already-running fleet, send the `registerThing` command to the live Worker instead: ```js const { createMdkClient } = require('@tetherto/mdk/backend/core/client') const client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }) await client.connect() await client.sendWorkerCommand('antminer-rack-1', null, 'registerThing', { id: 'AM-002', info: { container: 'site-1', serialNum: 'AM-002' }, opts: { address: '192.168.1.21', port: 80, username: 'root', password: 'root' } }) ``` `registerThing` persists the device config immediately, but the running Worker does not pick it up until it is stopped and restarted (`await worker.stop()`, then call `startAntminerWorker` again with the same `storeDir` and no `seedDevices`) — there is no hot-add. Before running in a deployment, generate the Worker config (`common.json` for Worker identity, `base.thing.json` for device defaults and per-model alert thresholds): ```bash cd backend/workers/miners/antminer ./setup-config.sh ``` For the full `seedDevices`/`registerThing` option reference and the mock `createServer` options, see the Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).
## Troubleshooting The development example on this page is `examples/backend/miners/antminer/index.js`. A working run prints the Kernel HRPC key and one line per registered device, then stays running until Ctrl+C. If it does not print those values, or if a mock port is already in use, follow [miner troubleshooting](/guides/miners/troubleshooting). ## Next steps - Decide how to run the Worker service — [Deployment topologies](/concepts/deployment-topologies) - Review telemetry units, command shapes, and error codes — [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/plugin/mdk-contract.json) # Run an Avalon Worker (/guides/miners/run-avalon-worker) ## Overview This page details how to run the Canaan Avalon Worker. Select the development (mock) or real-device path. ## Prerequisites Review [common deployment prerequisites](/guides/miners#prerequisites) before you start. Deployment-specific requirements: - A Node.js service or script in your deployment that runs the MDK Worker and registers devices - A supported Avalon device reachable from the machine or container running the Worker - The miner API reachable over the native CGMiner TCP API, typically port `4028` - No API username or password. The Avalon CGMiner API is unauthenticated ### Development
Run against a mock To support development, this repo ships a runnable example that boots a mock A1346, starts a Kernel and Gateway, and starts the Worker (`startAvalonWorker`) against it: ```bash node examples/backend/miners/avalon/index.js ``` It prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. For details on the boot options and mock, see [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md).
### Connect a miner #### 2.1 Confirm the model Avalon ships one model family today, `a1346` — confirm this against the [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md) as new models are added. #### 2.2 Register your miner Avalon devices use the native CGMiner TCP API on port 4028, which is unauthenticated (no username or password). Add this code to the Node.js service or script that runs the MDK Worker in your deployment. The snippet shows the minimum boot call seeding one Avalon device; replace the example IP address with your miner's value: ```js const { getKernel } = require('@tetherto/mdk') const { startAvalonWorker } = require('@tetherto/mdk-worker-avalon') const kernel = await getKernel() const worker = await startAvalonWorker({ workerId: 'avalon-rack-1', model: 'a1346', storeDir: './store/avalon-rack-1', seedDevices: [{ info: { container: 'site-1', serialNum: 'AV-001' }, opts: { address: '192.168.1.30', port: 4028 } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Make sure each miner's IP is reachable from the machine or container running the Worker before registering. Commands act on physical hardware — prioritize thermal safety. `seedDevices` only seeds a fresh, empty `storeDir` — once persisted, the device set survives restarts on its own. To add a device to an already-running fleet, send the `registerThing` command to the live Worker instead: ```js const { createMdkClient } = require('@tetherto/mdk/backend/core/client') const client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }) await client.connect() await client.sendWorkerCommand('avalon-rack-1', null, 'registerThing', { id: 'AV-002', info: { container: 'site-1', serialNum: 'AV-002' }, opts: { address: '192.168.1.31', port: 4028 } }) ``` `registerThing` persists the device config immediately, but the running Worker does not pick it up until it is stopped and restarted (`await worker.stop()`, then call `startAvalonWorker` again with the same `storeDir` and no `seedDevices`) — there is no hot-add. Before running in a deployment, generate the Worker config (`common.json` for Worker identity, `base.thing.json` for device defaults and per-model alert thresholds): ```bash cd backend/workers/miners/avalon ./setup-config.sh ``` For the full `seedDevices`/`registerThing` option reference and the mock `createServer` options, see the Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).
## Troubleshooting The development example on this page is `examples/backend/miners/avalon/index.js`. A working run prints `Kernel HRPC key:` and `Device:`, then stays running until Ctrl+C. If the example does not print both values, or if its mock port is already in use, follow [miner troubleshooting](/guides/miners/troubleshooting). ## Next steps - Decide how to run the Worker service — [Deployment topologies](/concepts/deployment-topologies) - Review telemetry units, command shapes, and error codes — [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/plugin/mdk-contract.json) # Run a Whatsminer Worker (/guides/miners/run-whatsminer-worker) ## Overview This page details how to run the MicroBT Whatsminer Worker. Select the development (mock) or real-device path. ## Prerequisites Review the [common deployment prerequisites](/guides/miners#prerequisites) before you start. Deployment-specific requirements: - A Node.js service or script in your deployment that runs the MDK Worker and registers devices - A supported Whatsminer device reachable from the machine or container running the Worker - The miner API reachable over encrypted TCP, typically port `14028` - The Whatsminer API password. The Worker negotiates a session token from it; there is no separate username ### Development
Run against a mock To support development, this repo ships a runnable example that boots a mock M56S Whatsminer, starts a Kernel, and starts the Worker (`startWhatsminerWorker`) against it: ```bash node examples/backend/miners/mdk.client.miner.js ``` It prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. To try another model, run that model's mock directly (`npm run mock ` from `backend/workers/miners/whatsminer`, or see [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md)) and adapt the `model` option in your own boot script.
### Connect a miner #### 2.1 Pick your model Use the Whatsminer Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md) to confirm the `model` value and mock `type` for your device. This guide uses `m56s`; replace it with the value for your miner. #### 2.2 Register your miner Whatsminer devices use an encrypted TCP API on port 14028 with token-based authentication; the Worker negotiates a session token from the device password (there is no separate username). Add this code to the Node.js service or script that runs the MDK Worker in your deployment. The snippet shows the minimum boot call seeding one Whatsminer device; replace the example IP address and password with your miner's values: ```js const { getKernel } = require('@tetherto/mdk') const { startWhatsminerWorker } = require('@tetherto/mdk-worker-whatsminer') const kernel = await getKernel() const worker = await startWhatsminerWorker({ workerId: 'whatsminer-rack-1', model: 'm56s', storeDir: './store/whatsminer-rack-1', seedDevices: [{ info: { container: 'site-1', serialNum: 'WM-001' }, opts: { address: '192.168.1.10', port: 14028, password: 'admin' } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Make sure each miner's IP is reachable from the machine or container running the Worker before registering. Commands act on physical hardware — prioritize thermal safety. `seedDevices` only seeds a fresh, empty `storeDir` — once persisted, the device set survives restarts on its own. To add a device to an already-running fleet, send the `registerThing` command to the live Worker instead: ```js const { createMdkClient } = require('@tetherto/mdk/backend/core/client') const client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }) await client.connect() await client.sendWorkerCommand('whatsminer-rack-1', null, 'registerThing', { id: 'WM-002', info: { container: 'site-1', serialNum: 'WM-002' }, opts: { address: '192.168.1.11', port: 14028, password: 'admin' } }) ``` `registerThing` persists the device config immediately, but the running Worker does not pick it up until it is stopped and restarted (`await worker.stop()`, then call `startWhatsminerWorker` again with the same `storeDir` and no `seedDevices`) — there is no hot-add. Before running in a deployment, generate the Worker config (`common.json` for Worker identity, `base.thing.json` for device defaults and per-model alert thresholds): ```bash cd backend/workers/miners/whatsminer ./setup-config.sh ``` For the full `seedDevices`/`registerThing` option reference, the mock `createServer` options, and the per-model alert blocks, see the Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).
## Troubleshooting The development example on this page uses `examples/backend/miners/mdk.client.miner.js`. A working run prints `Kernel HRPC key:` and `Device:`, then stays running until Ctrl+C. If the example does not print both values, or if its mock port is already in use, follow [miner troubleshooting](/guides/miners/troubleshooting). ## Next steps - Decide how to run the Worker service — [Deployment topologies](/concepts/deployment-topologies) - Review telemetry units, command shapes, and error codes — [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/plugin/mdk-contract.json) # Troubleshoot miner Workers (/guides/miners/troubleshooting) ## Overview This page covers the mock/development examples used by the Antminer, Whatsminer, and Avalon miner guides. The examples start a bundled mock miner, start an Kernel, register one device, print the identifiers you need, and keep running until you stop them. ## Expected output A working example prints an Kernel key and a registered device ID: ```text Kernel HRPC key: Device: Ctrl+C to stop. ``` If you do not see both `Kernel HRPC key:` and `Device:`, use the following checks. ## Find the right port Mock examples and real miners use different sources for ports. ### Mock examples Each runnable example starts a mock miner on the port declared in that example file. To find the mock port for your model: 1. Open the Worker's `USAGE.md` and choose the runnable example for your model: - Antminer: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md#runnable-examples) - Whatsminer: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md#runnable-examples) - Avalon: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md#runnable-example) 2. Open the matching `examples/run-*.js` file. 3. Look for the `createServer({ port: ... })` call. The cross-worker manifest also records the expected mock type and default port for each variant: [workers manifest](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/workers-manifest.yaml). ### Real miners Real devices use their native APIs: - Antminer: HTTP, usually port `80`, with digest-auth credentials. - Whatsminer: encrypted TCP, usually port `14028`, with the API password. - Avalon: CGMiner TCP API, usually port `4028`, with no username or password. Before registering a real miner, confirm the miner is reachable from the machine or container running the Worker. ## Clean up a mock port If an example exits with `EADDRINUSE` or says a port is already in use, find the process using that port: ```bash lsof -nP -iTCP: -sTCP:LISTEN ``` Replace `` with the mock port for your example. The output includes a process ID (`PID`). If the process is an old miner mock or example that you no longer need, stop it: ```bash kill ``` Run `lsof` again to confirm the port is free before restarting the example. ## Example does not print a Kernel key Same-process examples register Worker public keys directly and do not use DHT topic discovery. Runtime traffic still uses HRPC, which relies on HyperDHT to establish encrypted peer connections. The machine therefore needs outbound UDP access to its configured DHT bootstrap nodes even when Kernel and the Worker share a process or host. If outbound access or network-interface inspection is blocked, startup may stop responding or fail before printing `Kernel HRPC key:`. Check: - The machine has outbound network access. - Local security tooling, containers, or sandboxes are not blocking UDP/network-interface access. - You are running the command from the repository root. - Dependencies have been installed for `backend/core` and `backend/workers`. ## File lock or key file errors The examples call `getKernel()` with default local paths. By default, the topic file is `os.tmpdir()/mdk/.dht-topic` and the kernel key file is `os.tmpdir()/mdk/.kernel-key`. If another Kernel, gateway, or example is already running with the same defaults, you may see file lock errors, or clients may pick up the wrong Kernel key from the shared key file. Stop stale example processes before starting another example. If you need to run several examples side by side for development, run each process with a different temporary directory so each Kernel gets separate local state: ```bash TMPDIR=/tmp/mdk-antminer-s21 node backend/workers/miners/antminer/examples/run-s21.js ``` ## Still blocked When asking for help on [Discord](https://discord.com/invite/tetherdev) or [GitHub issues](https://github.com/tetherto/mdk/issues) collect: - The exact example command - The model and mock port - The full `stdout` and `stderr` - `node --version` and `npm --version` - Any process currently listening on the mock port # UI guides (/guides/ui) } title="React" href="/guides/ui/react" description="Compose reporting layouts using MDK React foundation components" /> } title="Core (headless)" href="/guides/ui/use-ui-foundation-headlessly" description="Use MDK UI Foundation headlessly, without the React adapter" /> # React UI guides (/guides/ui/react) } title="Compose reporting layouts" href="/guides/ui/react/compose-reporting-layouts" description="Build a custom reporting layout from the same building blocks the prebuilt reporting composites use" /> # Compose reporting layouts (/guides/ui/react/compose-reporting-layouts) @tetherto/mdk-react-devkit/foundation The reporting composites — [`Cost`](/reference/ui/components/dashboards), [`Ebitda`](/reference/ui/components/dashboards), [`EnergyBalance`](/reference/ui/components/dashboards), [`HashBalance`](/reference/ui/components/dashboards), and [`Hashrate`](/reference/ui/components/dashboards) — render fixed, opinionated layouts. When you need a different arrangement (a custom grid, a subset of charts, your own tabs), compose the page yourself from the **same building blocks** those composites are made of. Every building block receives pre-shaped data as props and does no fetching — wire your own data layer (RTK Query, TanStack, fixtures). ## When to use a building block vs the composite - Reach for the **composite** (for example ``) for the standard reporting page — fastest path, least wiring. - Reach for the **building blocks** when you need a custom layout, want only some panels, or are embedding a single chart in your own surface. ## Guides by composite - [Compose Cost layouts](/guides/ui/react/compose-reporting-layouts/cost) - [Compose EBITDA layouts](/guides/ui/react/compose-reporting-layouts/ebitda) - [Compose Energy balance layouts](/guides/ui/react/compose-reporting-layouts/energy-balance) - [Compose Hash balance layouts](/guides/ui/react/compose-reporting-layouts/hash-balance) - [Compose Hashrate layouts](/guides/ui/react/compose-reporting-layouts/hashrate) ## Shared building blocks These power the week selector inside [`TimeframeControls`](/reference/ui/components/dashboards), shared across the financial reporting surfaces. ### `TimeframeWeekFlatContent` ```tsx ``` Renders inside the week selector of [`TimeframeControls`](/reference/ui/components/dashboards). ### `TimeframeWeekTreeContent` ```tsx ``` Renders inside the week selector of [`TimeframeControls`](/reference/ui/components/dashboards). # Compose Cost layouts (/guides/ui/react/compose-reporting-layouts/cost) @tetherto/mdk-react-devkit/foundation The [`Cost`](/reference/ui/components/dashboards) composite renders a fixed 2x2 cost-summary layout. To build a custom arrangement, compose it from the building blocks below — each takes pre-shaped data as props and does no fetching. ## Building blocks ### `CostContent` ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards) composite. ### `CostMetrics` ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards) composite. ### `AvgAllInCostChart` ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards) composite. ### `ProductionCostChart` ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards) composite. ### `OperationsEnergyChart` ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards) composite. # Compose EBITDA layouts (/guides/ui/react/compose-reporting-layouts/ebitda) @tetherto/mdk-react-devkit/foundation The [`Ebitda`](/reference/ui/components/dashboards) composite renders a fixed EBITDA layout (metric row plus chart panel). To build a custom arrangement, compose it from the building blocks below — each takes pre-shaped data as props and does no fetching. ## Building blocks ### `EbitdaMetrics` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/dashboards) composite. ### `EbitdaCharts` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/dashboards) composite. ### `ActualEbitdaCard` ```tsx ``` Renders inside the [`EbitdaMetrics`](#ebitdametrics) row. ### `EbitdaHodlCard` ```tsx ``` Renders inside the [`EbitdaMetrics`](#ebitdametrics) row. ### `EbitdaSellingCard` ```tsx ``` Renders inside the [`EbitdaMetrics`](#ebitdametrics) row. ### `MonthlyEbitdaChart` ```tsx ``` Renders inside the [`EbitdaCharts`](#ebitdacharts) panel. ### `BitcoinPriceCard` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/dashboards) composite. ### `BitcoinProducedCard` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/dashboards) composite. ### `BitcoinProducedChart` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/dashboards) composite. ### `BitcoinProductionCostCard` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/dashboards) composite. # Compose Energy balance layouts (/guides/ui/react/compose-reporting-layouts/energy-balance) @tetherto/mdk-react-devkit/foundation The [`EnergyBalance`](/reference/ui/components/dashboards) composite renders a fixed two-tab layout (revenue and cost). To build a custom arrangement, compose it from the building blocks below — each takes pre-shaped data as props and does no fetching. ## Building blocks ### `EnergyBalanceRevenueCharts` ```tsx ``` Renders inside the revenue tab of [`EnergyBalance`](/reference/ui/components/dashboards). ### `EnergyBalanceRevenueMetrics` ```tsx ``` Renders inside the revenue tab of [`EnergyBalance`](/reference/ui/components/dashboards). ### `EnergyBalanceCostCharts` ```tsx ``` Renders inside the cost tab of [`EnergyBalance`](/reference/ui/components/dashboards). ### `EnergyBalanceCostMetrics` ```tsx ``` Renders inside the cost tab of [`EnergyBalance`](/reference/ui/components/dashboards). ### `EnergyBalancePowerChart` ```tsx ``` Renders inside both tabs of [`EnergyBalance`](/reference/ui/components/dashboards). ### `EnergyRevenueChart` ```tsx ``` Renders inside [`EnergyBalanceRevenueCharts`](#energybalancerevenuecharts). ### `EnergyCostChart` ```tsx ``` Renders inside [`EnergyBalanceCostCharts`](#energybalancecostcharts). ### `EnergyMetricCard` ```tsx ``` Renders inside the energy-balance metric grids. # Compose Hash balance layouts (/guides/ui/react/compose-reporting-layouts/hash-balance) @tetherto/mdk-react-devkit/foundation The [`HashBalance`](/reference/ui/components/dashboards) composite renders a fixed two-tab layout (revenue and cost). To build a custom arrangement, compose it from the two tab panels below — each takes pre-shaped data as props and does no fetching. ## Building blocks ### `HashBalanceRevenuePanel` ```tsx ``` Renders inside the revenue tab of [`HashBalance`](/reference/ui/components/dashboards). ### `HashBalanceCostPanel` ```tsx ``` Renders inside the cost tab of [`HashBalance`](/reference/ui/components/dashboards). # Compose Hashrate layouts (/guides/ui/react/compose-reporting-layouts/hashrate) @tetherto/mdk-react-devkit/foundation The [`Hashrate`](/reference/ui/components/dashboards) composite renders a fixed three-tab layout. To build a custom arrangement, compose it from the tab views below — each takes pre-shaped data as props and does no fetching. ## Building blocks ### `HashrateSiteView` ```tsx ``` Renders inside the Site View tab of [`Hashrate`](/reference/ui/components/dashboards). ### `HashrateMinerTypeView` ```tsx ``` Renders inside the Miner Type View tab of [`Hashrate`](/reference/ui/components/dashboards). ### `HashrateMiningUnitView` ```tsx ``` Renders inside the Mining Unit View tab of [`Hashrate`](/reference/ui/components/dashboards). # UI CLI reference (/guides/ui/ui-cli) The **UI CLI** (`mdk-ui`, package `@tetherto/mdk-ui-cli`) is the command surface your AI agent uses to build with MDK. You usually never run it yourself, your agent does, after you [wire your IDE](/quickstart/connect-agents). This page documents the commands for when you want to drive or inspect the tooling by hand. Every command runs locally and prints JSON by default. Add `--format table` for human-readable output. There are no network or model calls: each command is a lookup against files MDK ships. ## At a glance | Bucket | Section | What it covers | |--------|---------|----------------| | Set up | [Set up a project](#set-up-a-project) | Wire your IDE with `init` | | Discover | [Discover what to use](#discover-what-to-use) | `suggest`, `hooks`, `stores`, `find` | | Read | [Read a component contract](#read-a-component-contract) | `docs`, `example` | | Recipes | [Follow a recipe](#follow-a-recipe) | `blueprints`, `blueprint` | | Scaffold | [Scaffold and verify](#scaffold-and-verify) | `add page`, `check`, `sync` | | Inspect | [Inspect the UI CLI itself](#inspect-the-ui-cli-itself) | `--json-help` | ## All commands | Command | Summary | |---------|---------| | [`init`](#init) | Bootstrap `.mdk/context.md` and IDE rules | | [`suggest`](#suggest) | Ranked shortlist from free-text intent | | [`hooks`](#hooks) | List adapter hooks (optional `--category`) | | [`stores`](#stores) | List Zustand stores and query helpers | | [`find`](#find) | Filter components by domain and capability | | [`docs`](#docs) | Print a component's `USAGE.md` | | [`example`](#example) | Print a runnable `*.example.tsx` | | [`blueprints`](#blueprints) | List curated intent-to-component recipes | | [`blueprint`](#blueprint) | Show one recipe in detail | | [`add page`](#add-page) | Scaffold a page with chosen components | | [`check`](#check) | Type-check a file against real APIs | | [`sync`](#sync) | Refresh `.mdk/context.md` | | [`--json-help`](#json-help) | Machine-readable CLI surface | ## The agent's decision flow Given an intent, the deterministic path a session follows is: ```mermaid flowchart TD intent["Plain-language intent"] suggest["mdk-ui suggest"] state{"State or hooks needed?"} hooks["mdk-ui hooks / stores"] blueprints["mdk-ui blueprints"] match{"Matching blueprint?"} blueprint["mdk-ui blueprint"] find["mdk-ui find"] docs["mdk-ui docs / example"] add["mdk-ui add page"] check["mdk-ui check"] intent --> suggest suggest --> state state -->|yes| hooks state -->|no| blueprints hooks --> add blueprints --> match match -->|yes| blueprint match -->|no| find blueprint --> docs find --> docs docs --> add add --> check ``` ## Set up a project ### init Bootstraps the current project with an agent-context file and an IDE rule so every AI session is wired automatically: ```bash npx @tetherto/mdk-ui-cli init --ide cursor # .mdk/context.md + .cursor/rules/mdk.mdc npx @tetherto/mdk-ui-cli init --ide claude # .mdk/context.md + CLAUDE.md ``` ## Discover what to use ### suggest Turns free text into a ranked shortlist across components, hooks, blueprints, and stores: ```bash mdk-ui suggest "show hashrate for a pool" ``` ### hooks Lists every hook exported from `@tetherto/mdk-react-adapter`, grouped by category (`store`, `utility`, `permission`, `ui`, `external`): ```bash mdk-ui hooks --format table # all adapter hooks mdk-ui hooks --category store --format table # store-binding hooks only ``` ### stores Describes the Zustand stores and TanStack Query helpers from `@tetherto/mdk-ui-foundation`: ```bash mdk-ui stores --format table # stores and query helpers mdk-ui stores --category devices --format table ``` ### find Filters the component library by domain and capability: ```bash mdk-ui find --domain mining-operations --capability hashrate-monitoring ``` ## Read a component contract ### docs Prints a component's usage notes: ```bash mdk-ui docs LineChartCard ``` ### example Prints a runnable example: ```bash mdk-ui example LineChartCard ``` ## Follow a recipe Blueprints are curated recipes that map a high-level intent to a concrete set of components and hooks. ### blueprints Lists available recipes: ```bash mdk-ui blueprints ``` ### blueprint Shows one recipe in detail: ```bash mdk-ui blueprint device-management ``` ## Scaffold and verify ### add page Scaffolds a page with the components you name: ```bash mdk-ui add page Dashboard --component LineChartCard ``` ### check Confirms a file compiles against the real component APIs: ```bash mdk-ui check src/pages/Dashboard.tsx ``` ### sync Keeps the `.mdk/context.md` agent-context file current as MDK updates: ```bash mdk-ui sync ``` ## Inspect the UI CLI itself ### json-help `--json-help` prints the full command surface, useful for meta-tooling that wants to discover commands without running them: ```bash mdk-ui --json-help ``` ## Next steps - [Build dashboards with your AI agent](/quickstart/connect-agents): the two-step flow most developers use - [UI Devkit](/reference/ui): the component library these commands draw from - [MDK repositories](/support/resources/repositories): source for the UI CLI and the agent-ready contract # Use UI Foundation headlessly (/guides/ui/use-ui-foundation-headlessly) @tetherto/mdk-ui-foundation [`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation) is the framework-agnostic headless layer of the MDK App Toolkit. This how-to walks through installing it on its own and driving its Zustand stores from a non-React runtime — a Node script, a Vue or Svelte adapter you're authoring, a CLI tool, or a test helper. ## When to reach for this Use headless UI Foundation when: - You're authoring a framework adapter (Vue, Svelte, Web Components) and need raw access to the Zustand stores. - You're building a Node CLI or backend service that has to read MDK telemetry and act on it. - You're writing test helpers or fixtures that need to seed and inspect store state without a React renderer. - You need to subscribe to store changes from non-UI code — logging, websocket bridges, metrics. For a React app, the [React adapter](/tutorials/ui/react) wraps UI Foundation with `` and adapter hooks. Use that path instead so most React code never touches `@tetherto/mdk-ui-foundation` directly. ## Install `@tetherto/mdk-ui-foundation` has no peer dependencies on React or any UI framework. ```bash npm install @tetherto/mdk-ui-foundation ``` ## Subpath imports Pull only the pieces you need from the relevant subpath. Subpath imports give tree-shakers a smaller surface than the top-level barrel: ```ts ``` The [subpath exports table](/reference/app-toolkit/ui-foundation#subpath-exports) on the reference lists every supported entry. ## Create a QueryClient `createMdkQueryClient` returns a TanStack Query Core client wired to your Gateway. Pass an explicit `baseUrl`, or let the factory resolve one from environment variables: ```ts const queryClient = createMdkQueryClient({ baseUrl: 'https://app-node.example.com', }) ``` Without an explicit `baseUrl`, the factory checks `VITE_MDK_API_URL` then `MDK_API_URL` before falling back to `http://localhost:3000`. The [resolution order](/reference/app-toolkit/ui-foundation#queryclient-factory) on the reference covers every case. ## Read store state Each store is a Zustand vanilla singleton. `getState()` returns the current snapshot: ```ts const { token, permissions } = authStore.getState() console.log('current token', token) ``` ## Write store state `setState()` accepts either a partial object or a function that receives the previous state: ```ts devicesStore.setState({ selectedDeviceId: 'wm-002' }) devicesStore.setState((prev) => ({ devices: [...prev.devices, newDevice], })) ``` ## Subscribe to changes `subscribe()` runs a callback on every state change and returns an unsubscribe function: ```ts const unsubscribe = notificationStore.subscribe((state) => { console.log('unread notifications:', state.count) }) unsubscribe() ``` ## A complete Node example A small Node script that authenticates against the Gateway, fetches the device list once, and then tails unread notification count changes: ```ts authStore, devicesStore, notificationStore, } from '@tetherto/mdk-ui-foundation/store' async function main() { const queryClient = createMdkQueryClient({ baseUrl: process.env.MDK_API_URL ?? 'http://localhost:3000', }) authStore.setState({ token: process.env.MDK_TOKEN ?? '' }) const devices = await queryClient.fetchQuery({ queryKey: ['devices', 'list'], queryFn: async () => { const res = await fetch(`${process.env.MDK_API_URL}/api/devices`, { headers: { Authorization: `Bearer ${authStore.getState().token}` }, }) return res.json() }, }) devicesStore.setState({ devices }) console.log(`Found ${devices.length} devices`) const unsubscribe = notificationStore.subscribe((state) => { console.log(`unread notifications: ${state.count}`) }) process.on('SIGINT', () => { unsubscribe() process.exit(0) }) } main().catch((err) => { console.error(err) process.exit(1) }) ``` Run it with: ```bash MDK_TOKEN=ey... MDK_API_URL=https://app-node.example.com node script.ts ``` For the prebuilt query and mutation factories (`authQuery`, `devicesQuery`, `deviceQuery`, `telemetryQuery`), check the [QueryClient factory section](/reference/app-toolkit/ui-foundation#queryclient-factory) on the reference. ## Next steps - [UI Foundation reference](/reference/app-toolkit/ui-foundation): full store list, query helpers, and the `createMdkQueryClient` resolution order. - [MDK App Toolkit](/concepts/stack/app-toolkit): where UI Foundation fits in the frontend stack. - [React adapter](/tutorials/ui/react): if you decide to layer React on top. # Build a third-party Worker (/guides/workers/build-a-worker) # Build a third-party Worker This guide is for partners who want to integrate their own hardware, firmware, or data feed with MDK by shipping a Worker plugin package from their own public or private repository — no fork of this monorepo and no PR into `tetherto/mdk` required. It walks through building a Worker from scratch, end to end: the device client, the `mdk-contract.json`, the handlers, the mock, the tests, and finally how to depend on MDK's runtime from your own repo and register your Worker with a live Kernel. Everything below is a generalization of one real, runnable reference implementation already in this repo: [`backend/workers/samples/demo-worker/`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/index.js). It proves this pattern works with **zero** dependency on this monorepo's optional worker-infra services (provisioning stores, alert templates, stats aggregation) — just `WorkerRuntime` and the Worker Plugin contract shape. Keep it open as the minimal reference while you follow along. This guide links its corresponding files and adds production-oriented validation, recovery, and security boundaries that the deliberately small sample does not implement. This guide uses **partner integration** for the complete integration, **Worker plugin package** for the static contract and handlers, **host process** for the Node.js process that owns `WorkerRuntime`, and **device ID** for a runtime device identity. **Worker** is capitalized when it means the MDK component. Older APIs may still use `thing` for a device. For the broader architecture, read [How MDK works](/concepts/architecture). Also read the [Worker install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) and [Worker discovery model](/concepts/stack/workers). ## What you get ```text your-worker-repo/ package.json index.js # exports { plugin } — the Worker Plugin, nothing more lib/ device-client.js # plain I/O against your vendor's native API — no MDK concepts plugin/ index.js # the Worker Plugin: { contract, dir, connect, disconnect? } mdk-contract.json # the engineering + AI-context contract src/ telemetry/*.js # one handler per telemetry field commands/*.js # one handler per command mock/ server.js # a standalone fake of the vendor's device API tests/ unit/plugin.test.js # drives plugin.connect() + handlers directly against the mock # — no WorkerRuntime involved ``` This is exactly the shape of [`demo-worker`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/index.js) with the vendor name genericized: a package that can be fully built and tested with **zero** dependency on `WorkerRuntime`. Nothing about this layout is enforced by the framework — `WorkerRuntime` only cares about the plugin object it receives (`{ contract, dir, connect, disconnect? }`) — but matching it keeps your package legible to anyone who has read another MDK Worker. ## Prerequisites - Node.js `>=24` (all MDK core packages declare this `engines` constraint) - A device or firmware API you can talk to from Node — HTTP, TCP, Modbus, MQTT, serial, whatever your hardware speaks - Comfort with plain async JS — no MDK-specific framework knowledge is required to write the device client ### Scaffold the package Create your own repo (or a directory inside your existing one) with a `package.json`. Pick your own npm scope (as an external Worker provider, you will publish under a different domain to `@tetherto`): ```json { "name": "@your-org/mdk-worker-vendor", "version": "0.1.0", "description": "MDK Worker for Vendor firmware v1 devices", "license": "Apache-2.0", "engines": { "node": ">=24" }, "type": "commonjs", "scripts": { "lint": "standard", "test": "npm run lint && npm run test:unit", "test:unit": "NODE_ENV=test brittle tests/unit/*.test.js" }, "dependencies": { "debug": "^4.4.1" }, "devDependencies": { "brittle": "^3.16.0", "standard": "^17.1.2" } } ``` The examples and current plugin loader use CommonJS: plugin and handler files are loaded with `require()`. Set `"type": "commonjs"` or use `.cjs` files. An ESM-only package (`"type": "module"` with `.js` handlers) is not a supported plugin-loader path today. `WorkerRuntime` brings its own transport dependencies (`@hyperswarm/rpc`, `hyperswarm`, `hyperdht`) — you don't redeclare them. `brittle` and `standard` are the repository's test and lint tools; substitute your own tooling if you prefer. ### Write the device client This is the part that's actually yours: plain I/O against your vendor's native API. No MDK concepts, no base classes — just a function that returns an object with methods your handlers will call. `lib/device-client.js`, modeled on [`demo-worker/plugin/lib/device-client.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/plugin/lib/device-client.js): ```js "use strict"; function createClient({ host, port, timeoutMs = 5000 }) { const base = `http://${host || "127.0.0.1"}:${port}`; const call = async (path, opts = {}) => { try { const res = await fetch(base + path, { ...opts, signal: opts.signal || AbortSignal.timeout(timeoutMs), }); const body = await res.json(); if (!res.ok || body.ok === false) { throw new Error(body.error || `ERR_DEVICE_CALL_FAILED: ${res.status}`); } return body; } catch (err) { if (err.name === "TimeoutError") throw new Error(`ERR_DEVICE_TIMEOUT: ${path}`); throw err; } }; return { getSummary: () => call("/api/v1/summary"), reboot: () => call("/api/v1/reboot", { method: "POST" }), setPowerMode: (mode) => call("/api/v1/power-mode", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ mode }), }), }; } module.exports = { createClient }; ``` Whatever your device speaks — HTTP+digest auth, Modbus TCP, MQTT, a binary serial protocol — it lives entirely in this one file. Everything downstream only ever calls the methods this returns. Use a finite timeout for every device operation and propagate cancellation when the underlying client supports it. Retry idempotent telemetry reads only when the device protocol makes that safe, with bounded exponential backoff and structured logging owned by the host process. Do **not** automatically retry physical commands: a timeout can mean the command succeeded but its response was lost, so retrying can duplicate the operation. ### Declare the contract `mdk-contract.json` is the static source of truth for what telemetry your Worker reports, what commands it accepts, and the semantic context an AI agent or human operator needs to use it safely. The [formal JSON Schema](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) describes this handler-bearing source contract. Runtime device IDs and connection config belong to the host process and are reported dynamically during identity registration; they are deliberately not embedded in the plugin contract. `plugin/mdk-contract.json`: ```json { "metadata": { "provider": "vendor", "deviceFamily": "miner", "brand": "Vendor", "modelsSupported": ["VENDOR_Q1"], "overview": "Controls Vendor miners running firmware v1's HTTP JSON API. Operations affect physical hardware — prioritize thermal safety." }, "capabilities": { "telemetry": [ { "name": "hashrate_rt", "unit": "TH/s", "type": "number", "handler": "src/telemetry/hashrate-rt.js", "description": "Real-time hashrate from /api/v1/summary." }, { "name": "power", "unit": "W", "type": "number", "handler": "src/telemetry/power.js", "description": "Current power draw." }, { "name": "temperature", "unit": "C", "type": "number", "handler": "src/telemetry/temperature.js", "description": "Hash board temperature. Above 85C requires intervention." } ], "commands": [ { "name": "reboot", "handler": "src/commands/reboot.js", "description": "Restarts the miner controller.", "constraints": "Do not call more than once per 5 minutes.", "params": [] }, { "name": "setPowerMode", "handler": "src/commands/set-power-mode.js", "description": "Changes the power mode.", "params": [ { "name": "mode", "type": "string", "required": true, "enum": ["eco", "normal", "high"] } ] } ], "health": { "supportedStates": ["OK", "DEGRADED", "OFFLINE"], "alerts": ["alert.overheat"], "troubleshooting": [ "If alert.overheat, verify fan speeds and ambient temperature before rebooting." ] }, "errors": { "ERR_MODE_REQUIRED": "The requesting client omitted the required power mode.", "ERR_MODE_TYPE": "The supplied power mode was not a string.", "ERR_BAD_POWER_MODE": "The supplied power mode is not allowed or the firmware rejected it.", "ERR_COMMAND_COOLDOWN": "The command was issued before its declared cooldown elapsed.", "ERR_COMMAND_IN_PROGRESS": "A command of this type is already running for the device.", "ERR_DEVICE_TIMEOUT": "The device operation exceeded its configured timeout.", "ERR_DEVICE_CALL_FAILED": "The v1 HTTP API call failed or returned an error." } } } ``` A few fields worth calling out because they aren't just documentation: - `description` is read by AI agents as the semantic boundary for that field — put the actual constraint in it (e.g. _"Above 85C requires intervention"_), not just a label. - `params`, `enum`, numeric ranges, and `constraints` are published metadata; `WorkerRuntime` normalizes positional parameters but does not validate or enforce them. The command handler must reject missing, wrong-type, out-of-range, or disallowed values with stable `ERR_*` failures and enforce every declared cooldown. - `errors` maps your device's error codes to human-readable text; throw `Error` messages that contain these codes so operators and agents can look them up. - `health.alerts` is optional because a plugin without an alerting layer must not invent alerts. `metadata`, `capabilities.telemetry`, `capabilities.commands`, `capabilities.health.supportedStates`, and `capabilities.errors` are publication/catalogue requirements. At runtime, the current loader's minimum is looser: it requires `metadata` and `capabilities` objects plus valid handler entries. Treat the schema as the partner publication contract and the loader checks as fail-fast runtime validation, not two alternative formats. ### Write the telemetry and command handlers Every `handler` path in the contract resolves (relative to the plugin's directory) to a function with a fixed signature. The plugin loader `require()`s every declared handler eagerly at construction time — a missing file, a non-function export, or a duplicate name throws immediately, before your Worker ever starts (`ERR_PLUGIN_HANDLER_NOT_FOUND`). **Every entry in `capabilities.telemetry` and `capabilities.commands` needs a matching file** — declaring `power` / `temperature` / `reboot` in the contract without writing those handlers will fail as soon as the host process constructs `WorkerRuntime`. **Telemetry handler** — `async (ctx, params) => value`. `ctx` is `{ deviceId, device, config, services }`. The context object is shallow-frozen: handlers cannot replace its four top-level properties, but nested objects are not made immutable. The host owns `services`, the plugin's `connect()` result owns `device`, and the host owns `config`; handlers should treat `config` as read-only and mutate device state only through the device client's explicit methods. One file per telemetry field from Step 3: `plugin/src/telemetry/hashrate-rt.js`: ```js "use strict"; module.exports = async (ctx) => (await ctx.device.getSummary()).hashrate_ths; ``` `plugin/src/telemetry/power.js`: ```js "use strict"; module.exports = async (ctx) => (await ctx.device.getSummary()).power_w; ``` `plugin/src/telemetry/temperature.js`: ```js "use strict"; module.exports = async (ctx) => (await ctx.device.getSummary()).board_temp_c; ``` **Command handler** — `async (ctx, params) => result`. Return value becomes `payload.result`; a thrown `Error` becomes `{ status: 'FAILED', error: err.message }` in the response — this is how your `errors` map in the contract actually reaches the requesting client. One file per command from Step 3: `plugin/src/commands/reboot.js`: ```js "use strict"; const COOLDOWN_MS = 5 * 60 * 1000; const policyByDevice = new Map(); function audit(ctx, outcome, errorCode) { console.info( JSON.stringify({ event: "physical_command", command: "reboot", deviceId: ctx.deviceId, outcome, ...(errorCode ? { errorCode } : {}), }), ); } function stableErrorCode(err) { const match = /ERR_[A-Z0-9_]+/.exec(err && err.message); return match ? match[0] : "ERR_DEVICE_CALL_FAILED"; } module.exports = async (ctx) => { const now = Date.now(); const policy = policyByDevice.get(ctx.deviceId) || { lastAttemptAt: 0, running: false, }; if (policy.running) { audit(ctx, "rejected", "ERR_COMMAND_IN_PROGRESS"); throw new Error("ERR_COMMAND_IN_PROGRESS: reboot"); } const remaining = COOLDOWN_MS - (now - policy.lastAttemptAt); if (remaining > 0) { audit(ctx, "rejected", "ERR_COMMAND_COOLDOWN"); throw new Error(`ERR_COMMAND_COOLDOWN: reboot ${remaining}ms`); } // Record the attempt before device I/O. A failed or timed-out reboot still // consumes the cooldown because the device may have accepted the command. policy.lastAttemptAt = now; policy.running = true; policyByDevice.set(ctx.deviceId, policy); audit(ctx, "started"); try { const result = await ctx.device.reboot(); audit(ctx, "succeeded"); return result; } catch (err) { audit(ctx, "failed", stableErrorCode(err)); throw err; } finally { policy.running = false; } }; ``` `plugin/src/commands/set-power-mode.js`: ```js "use strict"; const ALLOWED_MODES = new Set(["eco", "normal", "high"]); function audit(ctx, outcome, errorCode) { console.info( JSON.stringify({ event: "physical_command", command: "setPowerMode", deviceId: ctx.deviceId, outcome, ...(errorCode ? { errorCode } : {}), }), ); } function stableErrorCode(err) { const match = /ERR_[A-Z0-9_]+/.exec(err && err.message); return match ? match[0] : "ERR_DEVICE_CALL_FAILED"; } function reject(ctx, code) { audit(ctx, "rejected", code); throw new Error(code); } module.exports = async (ctx, params) => { if (!params || params.mode === undefined) reject(ctx, "ERR_MODE_REQUIRED"); if (typeof params.mode !== "string") reject(ctx, "ERR_MODE_TYPE"); if (!ALLOWED_MODES.has(params.mode)) reject(ctx, "ERR_BAD_POWER_MODE"); audit(ctx, "started"); try { const result = await ctx.device.setPowerMode(params.mode); audit(ctx, "succeeded"); return result; } catch (err) { audit(ctx, "failed", stableErrorCode(err)); throw err; } }; ``` For a numeric parameter declared with `"min": 0, "max": 100`, enforce both type and range explicitly and add both codes to `capabilities.errors`: ```js if (typeof params.percent !== "number" || !Number.isFinite(params.percent)) { throw new Error("ERR_PERCENT_TYPE"); } if (params.percent < 0 || params.percent > 100) throw new Error("ERR_PERCENT_RANGE"); ``` The maps above are deliberately process-local teaching state. If a physical cooldown must survive restarts or multiple Worker hosts, store `lastAttemptAt` in process-owned persistent storage and update it atomically before device I/O. The JSON audit lines demonstrate the minimum event shape, including rejected and failed outcomes; production hosts must send these events to a durable audit sink. Actor identity and request correlation are owned by the authenticated Gateway/control plane because they are not currently present in the handler context. Never include credentials or raw device responses in audit events. Telemetry routing uses `query.type`, not the contract entry's return `type`. A request with `{ query: { type: "metrics" } }` invokes **every** telemetry handler and returns `{ metrics: { hashrate_rt: value, history: value, ... } }`; each handler error is isolated as `{ error: "..." }` under that key. A request with `{ query: { type: "history", limit: 20 } }` invokes only the telemetry entry named `history` and returns `{ name: "history", value }` or `{ error }`. The contract's `"type": "array"` describes the handler's returned value; it does not create the channel. A history-like handler is still included in the default `metrics` loop under the current runtime, so keep it bounded and inexpensive or change the runtime contract before relying on different behavior. Keep named-channel handlers defensive as callers can invoke them directly with untrusted query fields. ### Assemble the Worker plugin The [plugin](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) is the object `WorkerRuntime` is constructed with: the contract, the plugin's own directory (so handler paths resolve), and a `connect` function that turns one device's config into the `device` object every handler sees. An optional `disconnect` runs on `stop()`. `plugin/index.js`, modeled on [`demo-worker/plugin/index.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/plugin/index.js): ```js "use strict"; const { createClient } = require("../lib/device-client"); module.exports = { contract: require("./mdk-contract.json"), dir: __dirname, connect: async (config, { deviceId }) => { const device = createClient(config); // Probe once so an unreachable device is held offline from boot rather // than surfacing a connection error on every telemetry pull. await device.getSummary(); return device; }, // disconnect: async (device, { deviceId }) => { /* optional cleanup */ } // If you uncomment disconnect, put a comma after the connect function above. }; ``` A device whose `connect()` throws is held `offline` — requests to it return `ERR_DEVICE_UNAVAILABLE` — without taking down the runtime or its sibling devices. The current runtime has no reconnect loop: it calls `connect()` during `runtime.start()` only. The host process owns backoff, logging, and recovery policy; today, bringing an initially offline device online requires stopping and restarting that Worker host after restoring connectivity. ### Build a mock device Ship a standalone fake of your vendor's native API so anyone (including your own CI) can develop and test against your Worker without real hardware. It should know nothing about MDK — it's the same surface a real device on the LAN would present. `mock/server.js`, modeled on [`demo-worker/mock/server.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/mock/server.js): ```js "use strict"; const http = require("http"); function createServer({ host, port, hashrateThs, powerW }) { const state = { hashrateThs: hashrateThs || 180, powerW: powerW || 3400, boardTempC: 62, powerMode: "normal", }; const server = http.createServer((req, res) => { const reply = (code, body) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(body)); }; if (req.method === "GET" && req.url === "/api/v1/summary") { return reply(200, { hashrate_ths: state.hashrateThs, power_w: state.powerW, board_temp_c: state.boardTempC, power_mode: state.powerMode, }); } if (req.method === "POST" && req.url === "/api/v1/reboot") { return reply(200, { ok: true, rebooting: true }); } if (req.method === "POST" && req.url === "/api/v1/power-mode") { let buf = ""; req.on("data", (c) => { buf += c; }); req.on("end", () => { const { mode } = JSON.parse(buf || "{}"); state.powerMode = mode; reply(200, { ok: true, power_mode: mode }); }); return; } reply(404, { ok: false, error: "ERR_NOT_FOUND" }); }); server.listen(port, host || "127.0.0.1"); return { server, state, exit() { server.close(); }, }; } module.exports = { createServer }; ``` The mock must cover every device-client path your handlers call — summary fields for each telemetry handler, plus `/api/v1/reboot` for the reboot command (Step 2's `createClient` already defines that method). ### Test the plugin against the mock Drive `plugin.connect()` and the handler modules directly against the mock — this exercises your whole plugin (connection probing, telemetry translation, command dispatch, error mapping) with **no** `WorkerRuntime` in the loop, so it needs nothing beyond what you've already written in Steps 1–7. `demo-worker`'s own [`tests/unit/plugin.test.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/tests/unit/plugin.test.js) is the complete worked example of this style. ```js "use strict"; const test = require("brittle"); const plugin = require("../../plugin"); const hashrateRt = require("../../plugin/src/telemetry/hashrate-rt"); const reboot = require("../../plugin/src/commands/reboot"); const setPowerMode = require("../../plugin/src/commands/set-power-mode"); const vendorMock = require("../../mock/server"); test("telemetry and commands work against the mock", async (t) => { const auditEvents = []; const originalInfo = console.info; console.info = (line) => auditEvents.push(JSON.parse(line)); t.teardown(() => { console.info = originalInfo; }); const mock = vendorMock.createServer({ port: 9001, hashrateThs: 200 }); t.teardown(() => mock.exit()); const device = await plugin.connect( { host: "127.0.0.1", port: 9001 }, { deviceId: "vendor-0" }, ); const ctx = Object.freeze({ deviceId: "vendor-0", device, config: {}, services: null, }); t.is(await hashrateRt(ctx), 200, "hashrate_rt reads the mock"); const result = await setPowerMode(ctx, { mode: "eco" }); t.is(result.power_mode, "eco", "command reaches the mock"); await t.exception(() => setPowerMode(ctx, {}), /ERR_MODE_REQUIRED/); await t.exception(() => setPowerMode(ctx, { mode: 1 }), /ERR_MODE_TYPE/); await t.exception( () => setPowerMode(ctx, { mode: "turbo" }), /ERR_BAD_POWER_MODE/, ); const failingCtx = Object.freeze({ ...ctx, deviceId: "vendor-command-failure", device: { setPowerMode: async () => { throw new Error("ERR_DEVICE_CALL_FAILED"); }, }, }); await t.exception( () => setPowerMode(failingCtx, { mode: "eco" }), /ERR_DEVICE_CALL_FAILED/, ); t.ok( auditEvents.some( (e) => e.command === "setPowerMode" && e.outcome === "rejected", ), ); t.ok( auditEvents.some( (e) => e.command === "setPowerMode" && e.outcome === "failed", ), ); }); test("reboot enforces concurrency and cooldown after every attempt", async (t) => { let release; const pending = new Promise((resolve) => { release = resolve; }); const concurrentCtx = Object.freeze({ deviceId: "vendor-concurrent", device: { reboot: () => pending }, config: {}, services: null, }); const first = reboot(concurrentCtx); await t.exception(() => reboot(concurrentCtx), /ERR_COMMAND_IN_PROGRESS/); release({ ok: true }); await first; await t.exception(() => reboot(concurrentCtx), /ERR_COMMAND_COOLDOWN/); const failingCtx = Object.freeze({ deviceId: "vendor-failing", device: { reboot: async () => { throw new Error("ERR_DEVICE_CALL_FAILED"); }, }, config: {}, services: null, }); await t.exception(() => reboot(failingCtx), /ERR_DEVICE_CALL_FAILED/); await t.exception(() => reboot(failingCtx), /ERR_COMMAND_COOLDOWN/); }); ``` Cover at minimum: a telemetry handler reading a live value from the mock, a command reaching the mock and returning a result, required/type/range/enum validation, concurrent-command rejection, cooldown after successful and failed attempts, `connect()` throwing when the mock is unreachable, a firmware-side error surfacing with your contract's error code, and structured audit events containing rejected and failed outcomes. Production integration tests should also verify that the host forwards those events to its durable audit sink. ### Write a README Document, for your own package's users: what hardware/firmware it targets, how to run the bundled mock, and a link to your `mdk-contract.json` as the field reference. You don't need to follow this monorepo's internal `USAGE.md` + `examples/` documentation-catalogue convention (described here) — that exists to feed this repo's own generated hardware catalogue and docs-sync tooling, and doesn't apply to a package living outside it. ## Conformance checklist Before calling your Worker done: - [ ] `mdk-contract.json` validates against [`mdk-contract.schema.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json); every telemetry/command entry has a unique name and a CommonJS handler path that resolves to a function - [ ] Every `description` states the actual semantic boundary, not just a label — this is AI-reasoning surface, not decoration - [ ] Every device I/O operation has a finite timeout; safe read retries are bounded; physical writes are not automatically retried - [ ] Every command validates required values, types, ranges/enums, and declared cooldowns in the handler and maps failures to stable codes in `capabilities.errors` - [ ] Production command paths authenticate, authorize, rate-limit, optionally approve, and audit physical writes - [ ] Offline-at-start behavior and the host's restart/recovery policy are documented - [ ] The mock lets a new partner developer run the Worker with zero real hardware - [ ] Tests cover: a telemetry pull, a command that targets one device without touching its siblings, and a validation/device error surfacing as `status: 'FAILED'` - [ ] A [Kernel-mediated test](/guides/workers/test-a-worker) asserts the Worker reaches `READY`, exposes its device IDs, and serves telemetry through `createMdkClient` - [ ] `npm run lint` and your test suite are wired into your own CI ## Troubleshooting Plugin loading may fail synchronously during `new WorkerRuntime(plugin, opts)`: | Error | Diagnostic and remediation | | --------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `ERR_PLUGIN_REQUIRED` | The first argument is absent or not an object; pass the exported Worker Plugin | | `ERR_PLUGIN_CONNECT_NOT_FUNCTION` | Export `connect(config, { deviceId })` as a function | | `ERR_PLUGIN_DISCONNECT_NOT_FUNCTION` | Remove `disconnect` or export it as a function | | `ERR_PLUGIN_DIR_MISSING` | Set `plugin.dir` to a non-empty absolute directory such as `__dirname` | | `ERR_PLUGIN_CONTRACT_MISSING` | Export the parsed `mdk-contract.json` as `plugin.contract` | | `ERR_PLUGIN_CONTRACT_METADATA_MISSING` | `contract.metadata` is missing or not an object | | `ERR_PLUGIN_CONTRACT_CAPABILITIES_MISSING` | `contract.capabilities` is missing or not an object | | `ERR_PLUGIN_SECTION_NOT_ARRAY:
` | `capabilities.telemetry` or `capabilities.commands` must be an array | | `ERR_PLUGIN_ENTRY_NAME_MISSING:
` | Give every telemetry/command entry a non-empty string `name` | | `ERR_PLUGIN_HANDLER_MISSING:
.` | Add that entry's relative `handler` path | | `ERR_PLUGIN_HANDLER_NOT_FOUND:
.: : ` | Verify the displayed resolved path is relative to `plugin.dir`. Preserve and inspect the nested module error: the file may exist while one of its own imports is missing or incompatible | | `ERR_PLUGIN_HANDLER_NOT_FUNCTION:
.: ` | The CommonJS module must assign a function to `module.exports` | | `ERR_PLUGIN_DUPLICATE_NAME:
.` | Rename or remove the duplicate entry in that section | These fire only once a host process constructs `WorkerRuntime` with your plugin — for errors from that point on (runtime construction, Kernel registration, live requests), see [Troubleshooting](/guides/workers/test-a-worker) in Test a Worker with MDK. ## Next steps - Test your new [Worker's integration with MDK](/guides/workers/test-a-worker) - Understand the [security boundaries](/concepts/security-boundaries) Understand the end-user experience of controlling and monitoring your device via the Worker: - Build a [minimal dashboard](/tutorials/quickstart/build-a-dashboard) around one Worker - Run the [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md) with the supported Worker fleet - Connect an [AI agent via the MCP server](https://github.com/tetherto/mdk/blob/main/examples/full-site/docs/mcp-server.md) to query and command your Workers # Test a new Worker with MDK (/guides/workers/test-a-worker) This guide is for users of third-party worker packages or such partners who have integrated their own hardware, firmware, or data feed with MDK by shipping a [Worker plugin package](/guides/workers/build-a-worker). ## Overview Worker packages are the contract between the hardware and the Kernel, before relying on such a contract you will want to test its integration. To seed devices and register with Kernel, host the plugin on `WorkerRuntime` in a Node.js host process. The host module may live in the Worker plugin package itself; a second npm package is **not required**. A separate host directory is recommended when independent plugin publication and plugin-only tests are useful: ```text your-worker-host/ index.js # host module: WorkerRuntime, devices, lifecycle run-live.js # live Kernel registration and compatibility check ``` This mirrors [`examples/backend/demo-worker-caller/`](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js), which is an example directory containing one host module, not a standalone npm package. ## Prerequisites - Node.js `>=24` (all MDK core packages declare this `engines` constraint) - A completed [Worker Plugin package](/guides/workers/build-a-worker), including its bundled mock device - Comfort with plain async JS — no additional MDK framework knowledge is required beyond what building the package already covered ### Install MDK `@tetherto/mdk-worker` (the package that ships `WorkerRuntime`) is **not yet published to the npm registry** — MDK is pre-1.0 and still distributed as this monorepo. Until it is, the working path from an external repo is a git dependency plus a deep `require()` into the checked-out repo, exactly mirroring how every in-repo Worker already resolves it (by relative path, not through `node_modules` package resolution): ```bash npm install github:tetherto/mdk#main ``` This installs the whole monorepo under `node_modules/@tetherto/mdk` (its root `package.json` name). It does **not** auto-install the nested package's own dependencies — this repo's install is a federated set of scripts, not a single root dependency graph — so run its installer once after adding it: ```bash (cd node_modules/@tetherto/mdk/backend/core && ./install-packages.sh) ``` The same deep-path pattern also gets you `getKernel`, `startGateway`, and `waitForDiscovery` from `require('@tetherto/mdk/backend/core/mdk')`, used in Step 3 below. ### Write the host module `host/index.js`, modeled on [`examples/backend/demo-worker-caller/index.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js): ```js "use strict"; const { WorkerRuntime } = require("@tetherto/mdk/backend/core/mdk-worker"); const plugin = require("../your-worker-repo/plugin"); async function startVendorWorker({ workerId, kernelTopic, seedDevices }) { const runtime = new WorkerRuntime(plugin, { workerId, kernelTopic: kernelTopic || null, devices: (seedDevices || []).map((d) => ({ deviceId: d.id, config: d.opts, })), }); await runtime.start(); return { runtime, stop: () => runtime.stop(), }; } module.exports = { startVendorWorker }; ``` Required `WorkerRuntime` options are `workerId` and a non-empty `devices` array. Each `config` object is passed to the plugin's `connect()`. `kernelTopic` is needed only for DHT discovery. Without a `store`, `WorkerRuntime` generates a new RPC keypair on restart. Pass a process-owned store if deployment requires stable identity. The host process also owns persistence, sampling loops, retries, secrets, and shutdown. See the [demo host module](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js) for a SQLite sampler example. `WorkerRuntime` also exposes two read accessors for the host process: `getPublicKey()` returns the runtime's RPC public key (used to register with Kernel, shown in the next step), and `getDeviceContext(deviceId)` returns the same frozen `ctx` a handler receives (`{ deviceId, device, config, services }`) for a device that is currently `online`, or `null` otherwise — useful for wiring an external service (for example, a snapshot collector) to a live device outside the request/response cycle. ### Register directly with a live Kernel When Kernel and the Worker host share a process, register the runtime's public key directly. This complete `host/run-live.js` proves that Kernel accepted the Worker, that it reached `READY`, and that telemetry traverses the real client → Kernel → Worker path: ```js "use strict"; const os = require("os"); const path = require("path"); const { getKernel, waitForDiscovery, shutdown, } = require("@tetherto/mdk/backend/core/mdk"); const { createMdkClient } = require("@tetherto/mdk/backend/core/client"); const { startVendorWorker } = require("./index"); const vendorMock = require("../your-worker-repo/mock/server"); const ROOT = path.join(os.tmpdir(), `vendor-worker-${process.pid}`); function onceListening(mock) { if (mock.server.listening) return Promise.resolve(); return new Promise((resolve) => mock.server.once("listening", resolve)); } function withTimeout(promise, timeoutMs, code) { let timer; const timeout = new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error(code)), timeoutMs); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } async function main() { let mock; let worker; let kernel; let client; try { mock = vendorMock.createServer({ host: "127.0.0.1", port: 9001, hashrateThs: 200, }); await onceListening(mock); kernel = await getKernel({ root: ROOT }); worker = await startVendorWorker({ workerId: "vendor-demo", seedDevices: [ { id: "vendor-0", opts: { host: "127.0.0.1", port: 9001 } }, ], }); await kernel.registerWorker(worker.runtime.getPublicKey()); const workers = await waitForDiscovery(kernel, { minWorkers: 1, timeoutMs: 30000, }); const ready = workers.find( (w) => w.workerId === "vendor-demo" && w.state === "READY", ); if (!ready || !ready.deviceIds.includes("vendor-0")) { throw new Error("ERR_WORKER_NOT_READY"); } client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }); await client.connect(); const telemetry = await withTimeout( client.pullTelemetry("vendor-0", "metrics"), 8000, "ERR_TELEMETRY_TIMEOUT", ); if (typeof telemetry.metrics?.hashrate_rt !== "number") { throw new Error("ERR_TELEMETRY_INVALID"); } console.log(`READY ${ready.workerId}: ${ready.deviceIds.join(", ")}`); console.log(`hashrate_rt=${telemetry.metrics.hashrate_rt}`); } finally { if (client) await client.close(); if (kernel) await shutdown(kernel); if (worker) await worker.stop(); if (mock) mock.exit(); } } main().catch((err) => { console.error(err); process.exitCode = 1; }); ``` Expected output: ```text READY vendor-demo: vendor-0 hashrate_rt=200 ``` The timeout wrapper bounds the client's wait but cannot cancel the current HRPC request. Always close the client during shutdown. Device-protocol cancellation is separately owned by the device client from Step 2. ### Use DHT discovery across processes or hosts For DHT discovery, generate and securely distribute one 32-byte hex topic, start the Worker first with `kernelTopic`, then start Kernel with the same `topic`. Do **not** also call `registerWorker()`: ```js "use strict"; const crypto = require("crypto"); const os = require("os"); const path = require("path"); const { getKernel, waitForDiscovery, shutdown, } = require("@tetherto/mdk/backend/core/mdk"); const { startVendorWorker } = require("./index"); const ROOT = path.join(os.tmpdir(), `vendor-worker-dht-${process.pid}`); async function main() { const topic = process.env.MDK_TOPIC || crypto.randomBytes(32).toString("hex"); let worker; let kernel; try { worker = await startVendorWorker({ workerId: "vendor-demo", kernelTopic: topic, seedDevices: [ { id: "vendor-0", opts: { host: "10.0.0.20", port: 9001 } }, ], }); kernel = await getKernel({ root: ROOT, topic }); const workers = await waitForDiscovery(kernel, { minWorkers: 1, timeoutMs: 45000, }); const ready = workers.find( (w) => w.workerId === "vendor-demo" && w.state === "READY", ); if (!ready) throw new Error("ERR_WORKER_NOT_READY"); console.log(`READY ${ready.workerId}: ${ready.deviceIds.join(", ")}`); } finally { if (kernel) await shutdown(kernel); if (worker) await worker.stop(); } } main().catch((err) => { console.error(err); process.exitCode = 1; }); ``` For separate production processes, each process must install signal handlers and close every handle it owns. DHT topics enable rendezvous; they are not authentication secrets or command-authorization tokens. See the [discovery model](/concepts/stack/workers) for DHT, Local, and Same-process trade-offs. ## Troubleshooting ### Runtime construction | Error | Diagnostic and remediation | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `ERR_WORKER_ID_REQUIRED` | Pass a non-empty string `workerId` | | `ERR_DEVICES_REQUIRED` | Pass a non-empty `devices` array, unless this is an intentional provisioning-first host using `allowEmptyDevices` | | `ERR_DEVICE_ID_MISSING` | Every device spec needs a non-empty string `deviceId` | | `ERR_DEVICE_ID_DUPLICATE: ` | Device IDs must be unique within one runtime | | `ERR_DEVICE_CONFIG_INVALID: ` | `config`, when supplied, must be a non-null object | `allowEmptyDevices` opts a host into a provisioning-first bootstrap: the runtime constructs with zero devices instead of throwing `ERR_DEVICES_REQUIRED`, then takes `registerThing` writes (a built-in command — see [Worker Runtime legacy services](https://github.com/tetherto/mdk/blob/main/docs/reference/maintainers/worker-runtime-legacy-services.md)) that persist new device configs to the store. Those writes only take effect once the host is stopped and restarted with the provisioned set — there is no hot-add. It is off by default; every shipped miner Worker in this monorepo sets it to `true` in its boot function. ### Startup and discovery | Symptom | Diagnostic and remediation | | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | Device remains offline after `runtime.start()` | `connect()` failed. Check the device timeout, credentials, address, protocol, and redacted host logs. The current runtime does not reconnect; restore reachability and restart the Worker host | | `waitForDiscovery()` returns no `READY` Worker | For direct registration, await `runtime.start()` and `kernel.registerWorker(runtime.getPublicKey())`. For DHT, start the Worker first and verify both processes use the same 32-byte hex topic and can reach the DHT network | | Worker is present but never `READY` | Inspect identity and capability failures. Confirm at least one device ID is reported and the contract has valid `metadata` and `capabilities` | ### Request time | Error | Diagnostic and remediation | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ERR_DEVICE_UNAVAILABLE: ` | The device was held offline because `connect()` failed; follow the documented restart/recovery path | | `ERR_DEVICE_NOT_FOUND: ` | The request targeted an ID not seeded in this runtime; compare it with the Kernel registry's `deviceIds` | | `ERR_DEVICE_ID_REQUIRED: ` | A named telemetry pull or command omitted its target device ID | | `ERR_UNKNOWN_QUERY_TYPE: ` | Use `metrics` or the exact `name` of a telemetry entry; the entry's return `type` is not its channel name | | `ERR_UNKNOWN_COMMAND: ` | Use the exact declared command name and confirm its handler loaded | | `ERR_UNKNOWN_ACTION: ` | Use a public MDK client helper instead of constructing protocol actions manually | | Command returns `status: 'FAILED'` | Read the stable `ERR_*` value, check validation/cooldown/device logs, and do not retry a timed-out physical write until its actual device state is known | ## Next steps - Understand the [security boundaries](/concepts/security-boundaries) Understand the end-user experience of controlling and monitoring your device via the Worker: - Build a [minimal dashboard](/tutorials/quickstart/build-a-dashboard) around one Worker - Run the [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md) with the supported Worker fleet - Connect an [AI agent via the MCP server](https://github.com/tetherto/mdk/blob/main/examples/full-site/docs/mcp-server.md) to query and command your Workers # Quickstart (/quickstart) } title={Connect hardware} href="/quickstart/connect-hardware" description={ <> Drive a running MDK stack with a mock miner — live telemetry and commands over IPC ⏱️ ~3 min } /> } title={Build dashboards with your AI agent} href="/quickstart/connect-agents" description={ <> Use the CLI to wire Cursor or Claude to MDK with two commands, then build from plain-language prompts ⏱️ ~2 min } /> } title={Build dashboards with React} href="/quickstart/wire-react" description={ <> Install the three MDK React packages and wire MdkProvider ⏱️ ~3 min } /> } title={Explore the demo app} href="/tutorials/ui/explore-the-demo" description={ <> See the UI components rendered in a single demo app ⏱️ ~3 min } /> # Build dashboards with AI (/quickstart/connect-agents) MDK is built so your AI coding agent can build mining dashboards straight from plain-language prompts, without you wiring components by hand. There are two steps: wire your IDE once, then describe what you want. ## Wire your IDE once ### Run init Point the UI CLI at your IDE from your project root: ```bash npx @tetherto/mdk-ui-cli init --ide cursor ``` Use `--ide claude` for Claude. This writes a `.mdk/context.md` agent-context file and an IDE rule (`.cursor/rules/mdk.mdc` for Cursor, `CLAUDE.md` for Claude) so every AI session knows about MDK automatically. ### Prompt your agent Describe the dashboard you want in plain language: > Build me an operations dashboard with live hashrate and a device list. Your agent takes it from there: it finds the right components and hooks, scaffolds the page, and checks that it compiles. You review and run it. ## Next steps - [How agents build with MDK](/concepts/agents): why the results are trustworthy — the local manifests and stable-export contract behind the flow - [UI CLI reference](/guides/ui/ui-cli): every command your agent (or you) can run - [UI Devkit](/reference/ui): browse the component library your agent draws from # Connect hardware (/quickstart/connect-hardware) ## Overview This quickstart walks the shortest path from a fresh clone to a fully wired MDK stack you can drive interactively from a CLI. Everything runs in one Node process, no real hardware required. What you'll have at the end: - A mock Whatsminer M56S serving telemetry on `127.0.0.1:14028` - A Kernel with one Worker registered and one device discovered - An interactive `client.js` REPL talking to Kernel over its IPC socket — pull metrics, list Workers, send commands like `reboot` and `setpower` - (Optional) A Gateway HTTP API on `:3000` so non-Node consumers (browsers, AI agents over MCP) can hit the same stack over REST The example lives in [`examples/backend/mdk-e2e/`](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md#end-to-end-mdk-e2e) and contains six runnable scripts. This tutorial uses three of them: `run.js` for a smoke test, `server.js` for the long-running stack, and `client.js` for interactive control. ## Prerequisites - Node.js >=24 (LTS) - npm >=11 The stack starts a Kernel whose control plane is peer-to-peer over a Hyperswarm DHT, so it needs outbound network access. Without it, the stack stalls at startup while the Kernel tries to reach DHT bootstrap nodes. See [how Workers connect](/concepts/stack/workers) for the Kernel/DHT mechanics. ### Clone and install #### 1.1 Clone the repo ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` #### 1.2 Install dependencies The monorepo has two workspaces with their own dependency trees. Install both: ```bash backend/core/install-packages.sh ci backend/workers/install-packages.sh ci ``` Each script walks every `package.json` under its workspace and runs `npm ci`. The `examples/mdk-e2e/` package is included automatically — no extra install step needed.
(1.3 Optional) Smoke test the stack Before going interactive, prove the wiring works. `run.js` starts a mock Whatsminer + Worker + Kernel in one process, exercises a few queries, prints the results, and exits cleanly: ```bash node examples/backend/mdk-e2e/run.js ``` Expected output (the UUID and metric values vary): ```text Devices: [ '8f3e9a2b-7c1d-4e5a-9f8b-6c2d1e3f4a5b [miner-wm-m56s]' ] Telemetry: ONLINE hashrate=170000 power=3500W Commands: reboot, setPowerMode, setLED, setPowerPct, setupPools, saveComment ``` If you see those three lines, every layer is working: the mock is responding, the Worker registered the device, Kernel discovered the Worker over the local DHT topic, and IPC routing is delivering envelopes both ways. The script tears itself down and exits with code 0. If the smoke test fails with `EADDRINUSE` on port 14028, a previous run left a Node process alive. Kill stragglers with `pkill -f mdk-e2e` and retry.
### Run the interactive demo #### 3.1 Start the stack In your terminal: ```bash node examples/backend/mdk-e2e/server.js ``` `server.js` starts the same mock + Worker + Kernel as `run.js`, but stays running and prints the IDs you'll need: ```text Kernel key: 7a4c8b...e3f0 Device: 8f3e9a2b-7c1d-4e5a-9f8b-6c2d1e3f4a5b hp-rpc-cli -s 7a4c8b...e3f0 -m mdk -d '{...}' hp-rpc-cli -s 7a4c8b...e3f0 -m mdk -d '{...}' Ctrl+C to stop. ``` The `Kernel key` is a 64-char hex public key. `Device` is a UUIDv4 generated at registration time. Both vary per run — note the device UUID for the next step. The two `hp-rpc-cli` lines are paste-ready commands for inspecting Kernel over HRPC from another machine. You don't need them for this tutorial — they're there if you have [`hp-rpc-cli`](https://github.com/holepunchto/hp-rpc-cli) installed and want to go off-script. #### 3.2 Connect the interactive client Open a second terminal in the same `mdk` directory: ```bash node examples/backend/mdk-e2e/client.js ``` `client.js` connects to Kernel's default IPC socket and gives you an MDK REPL: ```text MDK Client — connected to IPC: /tmp/mdk/ork.sock Type "help" for commands, "quit" to exit. mdk> ``` #### 3.3 Drive the stack 3.3.1 Discover the telemetry of your (mock) device by entering commands at the `mdk>` prompt, substituting `` with the UUID printed in Step 3.1: ```text mdk> metrics ``` Each command builds an MDK Protocol envelope, writes it to Kernel's IPC socket, and prints the JSON response. 3.3.2 Try changing the power mode and observing the effect: ```text mdk> setpower low mdk> metrics ``` After `setpower ... low` the second `metrics` call should reflect the power mode change.
Full command reference ##### Reads ```text workers — list workers list [deviceId] — list devices count [deviceId] — device count metrics — live telemetry from hardware logs — recent logs settings [deviceId] — worker settings stats [deviceId] — fleet stats config — device config (pools, etc.) capabilities — mdk-contract capabilities state [deviceId] — worker state snapshot ``` ##### Commands ```text reboot — reboot miner setpower — set power mode (normal/low/high) setled [on|off] — toggle LED ``` ```text quit / exit — exit client ```
#### 3.4 Tear down When you're done, exit the client and stop the stack: ```text mdk> quit ``` Then `Ctrl+C` in Terminal 1.
### 4 (Optional) enable Gateway for HTTP access In **Terminal 1**, `Ctrl+C` the running stack, then restart with `--app-node`: ```bash node examples/backend/mdk-e2e/server.js --app-node ``` You'll see an extra line in the startup banner: ```text App-node: http://localhost:3000 (noAuth mode) ``` Confirm it's alive — Gateway has no index page, so hit `/auth/site` directly: ```bash curl http://localhost:3000/auth/site ``` You should see something like `{"site":"Site_Name"}`. In `noAuth` mode most data endpoints (e.g. `/auth/list-things`, `/auth/miners`) are unavailable — they require the cache and auth config that the full Gateway service sets up. `--app-node` in `server.js` is a development shortcut; for full REST access to device telemetry, run Gateway as a full service. See [`backend/core/app-node/`](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md) for setup. `--app-node` runs in `noAuth` mode for development convenience. Do not expose port 3000 outside localhost.
## What just happened Your stack wired up, in order: 1. **Mock device**:`server.js` calls `wmMock.createServer({ port: 14028, ... })` — an HTTP server speaking the Whatsminer protocol with canned telemetry. 2. **Kernel**: `getOrk()` boots the kernel, generates a random DHT topic, and opens an IPC socket at the default path (`os.tmpdir()/mdk/ork.sock`). 3. **Worker**: `startWorker(WM_M56S, { ork })` instantiates the Whatsminer manager, mounts its protocol adapter, and registers with Kernel directly — no DHT round-trip because they share the process. 4. **Thing registration**: `manager.registerThing({ info, opts })` tells the Worker about the device at `127.0.0.1:14028`. The Worker stores the registration and starts polling. 5. **Client**: `client.js` opens the IPC socket from a second process and sends MDK Protocol envelopes (`worker.list`, `telemetry.pull`, `command.request`, ...). Kernel routes them to the Worker, the Worker hits the mock, and the response flows back over IPC. No Gateway here? Right. Gateway is the translator that lets non-Node consumers — browser UIs, AI agents over MCP — speak MDK Protocol to Kernel. `client.js` already speaks MDK Protocol over IPC, so it talks to Kernel directly. Gateway becomes mandatory only when the consumer can't open a UNIX socket. See [`architecture.md#gateway`](/concepts/architecture#gateway). ## Cleanup `Ctrl+C` in Terminal 1 stops the Worker, Kernel, and mock cleanly. `run.js` deletes its own state directory on exit. `server.js` leaves data under `os.tmpdir()/mdk/` — safe to ignore, or remove with: ```bash rm -rf "$TMPDIR/mdk" /tmp/mdk ``` ## Next steps - Go deeper and [run the dashboard demo](/tutorials/full-stack/dashboard): put a browser dashboard on a running stack with live charts. - Or go simpler and walk the simpler [single-script Antminer path](/tutorials/backend-stack/run) - Run a [full site (multiple Workers and devices):](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-site/site.js) - Understand the [install pattern for any Worker](https://github.com/tetherto/mdk/blob/main/backend/workers/install-pattern.md) - Read all [runnable examples in one place](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md) # Wire React (/quickstart/wire-react) This page walks through the minimum integration of the MDK UI toolkit into a React application. ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ ## Install ```bash # Clone the MDK UI monorepo (adjust the URL to your fork if needed) git clone https://github.com/tetherto/mdk.git cd mdk/ui # Install dependencies and build packages (npm workspaces) npm install npm run build ``` Then add to your app's `package.json`: ```json { "dependencies": { "@tetherto/mdk-react-devkit": "*", "@tetherto/mdk-react-adapter": "*", "@tetherto/mdk-ui-foundation": "*" } } ``` [Wrap your app](/quickstart/wire-react) in `` from `@tetherto/mdk-react-adapter` when using connected foundation components or adapter store hooks. > **Coming soon** — npm packages are not yet published. Use the monorepo setup for now. ```bash npm install \ @tetherto/mdk-react-devkit \ @tetherto/mdk-react-adapter \ @tetherto/mdk-ui-foundation ``` Run `npm install` from the `mdk/ui` workspace root after your app is under `apps/` so npm links workspace packages. ## Wrap your app in MdkProvider `MdkProvider` sets up the TanStack `QueryClient` and the API base URL context. It is required for foundation hooks and components that read shared app state. ```tsx // main.tsx ReactDOM.createRoot(rootElement).render( , ) ``` ## Use the adapter hooks inside React Each hook subscribes the component to the relevant Zustand store and re-renders only when the selected slice changes. ```tsx const Toolbar = () => { const { permissions } = useAuth() const { selectedDevices } = useDevices() const { setAddPendingSubmissionAction } = useActions() // ... } ``` ## Or read / write stores directly outside React The vanilla stores expose `getState()` / `setState()` so utility code, side-effect handlers, and tests can interact with the same source of truth. ```tsx // Outside React (utilities, sagas, etc.) you can read/write directly: devicesStore.getState().setSelectedDevices([]) actionsStore.getState().setAddPendingSubmissionAction({ /* … */ }) ``` ## Theme via design tokens and @layer mdk The compiled stylesheet declares `@layer base`, `mdk`, `app` — so unlayered or `@layer app` styles in your application always win against devkit component styles. MDK ships with `--mdk-color-primary: #f7931a`; override tokens in `:root` only when reskinning. ```css /* app.css — imported AFTER @tetherto/mdk-react-devkit/styles.css */ :root { --mdk-color-primary: #f7931a; --mdk-radius: 6px; } @layer app { .mdk-button--variant-primary { letter-spacing: 0.04em; } } ``` See [Theme](/reference/ui) for design tokens and `@layer` override rules. ## Next steps - [Tutorial](/tutorials/ui/react/tutorial) — full integration walkthrough - [Hooks](/reference/ui/hooks) — state, component, and utility hooks - [Explore the demo](/tutorials/ui/explore-the-demo) — run the demo browser without adding MDK to your own app yet - [Core](/reference/ui/components) — building block reference (`@tetherto/mdk-react-devkit/core`) - [Foundation](/reference/ui/components) — mining-domain components (`@tetherto/mdk-react-devkit/foundation`) # Reference (/reference) The Reference section indexes the canonical specs for everything MDK exposes: field semantics, signatures, transition rules, and contracts. Reach for it when you need exact shapes. For narrative explanations, see [Architecture](/concepts/architecture); for step-by-step instructions, see [Get started](/quickstart). ## Browse by stack area ### App Toolkit - **[UI Devkit](/reference/app-toolkit/ui-devkit)**: constants, hooks, types, and utilities for the React UI Devkit - **[UI Foundation](/reference/app-toolkit/ui-foundation)**: framework-agnostic headless Zustand stores and TanStack Query factory - **[Hooks](/reference/ui/hooks)**: complete hook catalog: data, state, component, and utility hooks across the adapter and devkit packages ### Kernel - **[Kernel](/reference/kernel)**: kernel module specs, state machines, transition tables, and recovery behavior ### MDK Protocol - **[Protocol](/reference/protocol)**: envelope schema, request/response examples, action catalogue, and base command set - *Capability contract*: coming soon ### Hardware - **[Supported hardware](/reference/supported-hardware)**: miners, containers, power meters, sensors, and mining-pool integrations # Glossary (/reference/glossary) This page provides explanations for terms that new users may not be familiar with. - [Stack](#stack-and-hardware-terms) - [HRPC](#hyperswarm-rpc) ## Stack and hardware terms This section explains the terms you need to familiarize yourself with, using an Antminer rack as an example. | Term | What it is | Lives at | | --- | --- | --- | | **Kernel** (Orchestration Kernel) | The pull-only kernel that owns the device registry, routes commands, and aggregates telemetry. | [`backend/core/kernel/index.js`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/index.js) | | **Gateway** | The developer-owned entry point between non-Node clients (UI, AI agents) and Kernel. Mandatory whenever a non-Node consumer reaches the kernel; not used in the in-process Antminer-rack example below. | [`backend/core/gateway/`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/worker.js) | | **Worker** | A device-family translator. Speaks the MDK Protocol upward to Kernel and the vendor's native API downward to one device family (one miner brand, one container type, one pool API). | [`backend/workers/docs/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) | | **Manager class** | The JavaScript class a Worker exports, one per supported device model. Instances drive a single rack of devices. | e.g. `AM_S19XP`, `AM_S21` in [`backend/workers/miners/antminer/index.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/index.js) | | **Thing** | One registered device instance. Created by calling `manager.registerThing({ info, opts })`. Identified by a generated `deviceId`. | runtime, in `manager.mem.things` | ### How they compose, for an Antminer rack ```mermaid flowchart TB subgraph clientLayer ["Your code"] Client["your script (e.g., client.js)"] end subgraph kernel ["Kernel"] Kernel["Kernel
device registry · command routing · telemetry pull"] end subgraph workerLayer ["Antminer Worker"] AntminerWorker["e.g., AM_S21PRO"] end subgraph devices ["Antminer devices (real or mock)"] Miners["Antminers (HTTP / digest auth)"] end Client -->|"HRPC"| Kernel Kernel -->|"HRPC"| AntminerWorker AntminerWorker --> Miners ``` The same shape repeats for every other device family (Whatsminer, container vendors, pool APIs). For a multi-Worker view, parallel Workers, and multi-site deployments, see [`architecture.md#scaling`](/concepts/architecture#scaling). ## Hyperswarm RPC MDK uses [`@hyperswarm/rpc`](https://github.com/holepunchto/rpc) as its runtime transport. Hyperswarm RPC (HRPC) is not an HTTP-based RPC system. It is an RPC layer that rides on Hyperswarm peer-to-peer connectivity. The library is a simple RPC over the Hyperswarm DHT, backed by `Protomux`. Think of it as a peer-to-peer remote function call system built on a DHT and an encrypted connection layer. **Mental model** — Hyperswarm finds peers and establishes connections; `Protomux` divides the connection into named channels; RPC defines the conversation — a caller names a method and receives a reply. A useful analogy is a phone call between peers — Hyperswarm helps the phones find each other and connect; `Protomux` splits the line into channels; RPC defines how one side asks for a method and the other side responds. **Practical implications:** - You work with services, methods, requests, and responses — not URLs and routes - The RPC-shaped API is identical across same-process, same-host, and distributed deployments; only the discovery mechanism changes (same-process registration, shared directory, or DHT topic) - Peers discover and communicate without a central HTTP server ### HRPC on the same host MDK uses HRPC as the single transport across all deployment shapes — same-process, same-host, and distributed. Every component is addressed by its public key, not by a socket path or hostname. The Gateway, a standalone Node.js script, and a remote service all connect the same way: ```js createMdkClient({ hrpc: { key } }) ``` The Noise handshake that HRPC performs on every connection authenticates by key, so Kernel's allowlist works identically whether the caller is on the same machine or a remote host. This is consistent with the broader Holepunch ecosystem philosophy — everything is a peer addressed by public key. When the peer is on the same machine it routes locally over the local network interface; the application code sees no difference. ## Next steps - You are ready to run the example in [Run the stack](/tutorials/backend-stack/run) - Learn more about: - Multi-process discovery across machines: [Worker discovery](/concepts/stack/workers) - Gateway implementation details (HTTP routing, JWT auth, RBAC) — see [`backend/core/gateway/worker.js`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/worker.js) - Building your own Worker for a new device family: see [`backend/workers/docs/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) - Per-device contract details (telemetry units, command shapes, error codes): those live in each Worker's `mdk-contract.json`, e.g. [`backend/workers/miners/antminer/mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/plugin/mdk-contract.json) # Kernel reference (/reference/kernel) `@tetherto/mdk-kernel` is the orchestration kernel of the MDK stack. This subsection holds the canonical specs for its internal modules. For the architectural narrative explaining how these modules fit together, see [Kernel](/concepts/stack/kernel). ## What's documented - **[Modules](/reference/kernel/modules)**: per-module responsibility, interfaces, state machines, transition rules, crash-recovery procedures, and scaling characteristics. # Kernel modules (/reference/kernel/modules) `@tetherto/mdk-kernel`'s coordination splits across single-purpose modules. Each owns its own state machine, persistence boundary, and scaling characteristics. Six modules ship in v0.0.1; two more (Fault Supervisor, Concurrency Manager) are deferred to a later release. For the architectural overview that explains how these modules connect, see [Kernel](/concepts/stack/kernel). This page is the per-module spec. ## How to read this page Each accordion below covers one module: - **Responsibility**: what the module owns and the example commands or events it handles. - **Interfaces**: input and output channels, plus the public function names callers depend on. - **State machine**: the canonical diagram for the module's internal lifecycle. - **Crash recovery**: behavior on a fresh start, including how state is reconstructed. - **Scalability**: where the module can be extracted, sharded, or replicated. ## Modules **Responsibility**: validates incoming commands against the generic MDK schema, checks permissions, and resolves the correct Worker based on the `deviceId`. *Example*: when a Gateway sends a `reboot` command for `wm001`, the Dispatcher verifies that `wm001` exists, that `reboot` is a valid capability for it, then passes the request to the State Machine. **Interfaces**: - *Input*: receives generic commands via Holepunch RPC (HRPC) and the MDK Protocol from the Gateway or MCP Handler. - *Output*: hands off validated commands to the Command State Machine. - *Functions*: `dispatchCommand(deviceId, action, payload)` **State machine**: ```mermaid stateDiagram-v2 [*] --> Validating Validating --> RoutingAction : Valid Validating --> Rejected : Invalid RoutingAction --> Enqueued Enqueued --> [*] ``` **Crash recovery**: none needed. In-flight requests fail and must be retried by the client. **Scalability**: extracted easily; can run independently to offload validation. **Responsibility**: tracks the execution lifecycle of every single command in the system. Receives execution results directly via the synchronous HRPC response. If the connection drops or a response is delayed, it relies on the Scheduler to fetch the latest status via `state.pull` so commands cannot hang. *Example*: once the `reboot` command is dispatched it transitions to `EXECUTING`. If the HRPC response returns OK it transitions to `SUCCESS`. If the response hangs, the next `state.pull` fetches the true status from the Worker. **Interfaces**: - *Input*: receives validated commands from the Dispatcher; status updates from HRPC responses or Scheduler ticks. - *Output*: invokes the Worker HRPC execution layer; emits terminal state results to the caller. - *Functions*: `enqueue(command)`, `syncState(commandId)`, `cancel(commandId)` **State machine**: ```mermaid stateDiagram-v2 [*] --> QUEUED QUEUED --> DISPATCHED DISPATCHED --> EXECUTING : HRPC sent EXECUTING --> SUCCESS : state.pull (response) EXECUTING --> FAILED : state.pull (error) EXECUTING --> TIMEOUT TIMEOUT --> QUEUED : Retry allowed TIMEOUT --> FAILED : Max retries SUCCESS --> [*] FAILED --> [*] ``` **Crash recovery**: on startup, performs a recovery sweep of pending commands. `DISPATCHED` and `EXECUTING` commands are forced to `TIMEOUT` (and re-queued if retries are available); `QUEUED` commands are left untouched. **Scalability**: requires state sharding (for example, by device or rack). **Responsibility**: the phonebook for the entire `@tetherto/mdk-kernel` ecosystem. Maps which physical device IDs belong to which connected Worker channels and stores their declared capabilities. *Example*: a `whatsminer-worker` connects and declares it manages `wm001` and `wm002`. The Registry saves this topology so the Dispatcher knows exactly where to route a command for `wm001`. **Interfaces**: - *Input*: `identity.register` requests from Workers. - *Output*: internal events triggering full state lifecycle binding. - *Functions*: `resolveWorkerState(target)` **State machine**: ```mermaid stateDiagram-v2 [*] --> Unregistered Unregistered --> Discovered : DHT peer detected Discovered --> IdentitySaved : identity pulled IdentitySaved --> Ready : capabilities pulled Ready --> Terminated : eviction Terminated --> [*] ``` **Crash recovery**: rebuilt from state, which serves as a baseline to detect Workers that were registered but failed to reconnect. **Scalability**: read-heavy by nature; can be extracted into a read-replica architecture or partitioned by region or rack. **Responsibility**: a lightweight proxy and routing layer between the upper system (UI / AI) and the downstream Workers. Rather than `@tetherto/mdk-kernel` performing heavy time-series aggregations, the *Worker* is responsible for storing and aggregating data for the specific devices it controls. The Collector simply provides an interface to query this data and proxies the response up to the UI (via Gateway) or the AI Agent. **Worker data handling (telemetry context)**: - *Compaction*: the Worker handles compaction of metrics over large time frames. - *Local storage*: Workers should save telemetry data in a local Hyper DB. - *As-requested serving*: the Worker serves data strictly when `@tetherto/mdk-kernel` asks for it, precisely as dictated by the telemetry schemas in `mdk-contract.json`. - *Internal scheduling*: to achieve this without blocking `@tetherto/mdk-kernel`, the Worker may run its own internal scheduler for device polling. **Interfaces**: - *Input*: client or AI telemetry queries (for example, "fetch metrics for device wm001"). - *Output*: normalized telemetry payloads passed straight through from the Worker to the requesting layer. - *Functions*: `proxyTelemetryFetch(deviceId, queryArgs)` **State machine**: ```mermaid stateDiagram-v2 [*] --> Idle Idle --> Proxying : Request received Proxying --> RoutingToClient : Worker returns data Proxying --> Timeout : Worker unresponsive RoutingToClient --> Idle ``` **Scalability**: because the heavy lifting of data storage and aggregation is pushed down into the isolated Worker processes, the Collector remains stateless and highly scalable as a pure asynchronous router. **Responsibility**: the system metronome. Triggers repetitive tasks without holding any domain-specific logic itself. *Example*: emits an internal `tick` event every 5 seconds that the Health Monitor listens to, prompting it to ping all Workers. **Interfaces**: - *Input*: system clock and configured task intervals. - *Output*: injects intents (for example, `telemetry.pull`, `health.ping`) into the Dispatcher or Collector. - *Functions*: `addJob(interval, intent)`, `removeJob(jobId)` **State machine**: ```mermaid stateDiagram-v2 [*] --> Waiting Waiting --> Triggered : Interval elapsed Triggered --> Waiting ``` **Crash recovery**: timers re-initialize from zero on startup. Tasks are strictly idempotent. **Scalability**: scales trivially. Requires basic distributed locking to avoid duplicate ticks in multi-process `@tetherto/mdk-kernel` deployments. **Responsibility**: continuously evaluates the liveness and readiness of every registered Worker to prevent routing messages to dead nodes. *Example*: if `health.ping` to a Worker fails three times in a row, the Health Monitor marks the Worker's status as `SICK` and tells the Registry to halt routing new commands there. **Interfaces**: - *Input*: executes `health.ping` sequentially based on Scheduler ticks. - *Output*: pushes status updates to the Registry. - *Functions*: `pingWorker(workerId)`, `getHealth(workerId)` **State machine**: ```mermaid stateDiagram-v2 [*] --> UNKNOWN UNKNOWN --> HEALTHY : Ping success HEALTHY --> SICK : Ping failed (1) SICK --> DEAD : Ping failed (threshold) SICK --> HEALTHY : Ping success DEAD --> HEALTHY : Reconnected ``` **Crash recovery**: blank slate on startup; re-evaluates all known Workers immediately via ping. **Scalability**: operates locally per `@tetherto/mdk-kernel` kernel, or via independent lightweight ping agents. **Idea**: implements circuit-breaker patterns to protect the overall system from cascading failures caused by bad hardware or software bugs (for example, rejecting commands during a cooling period after repeated errors). **Status**: deferred for the first cut to keep the core orchestrator simple. If the use case arises (such as complex retry backoffs or cluster destabilization), it will be reintroduced. **Idea**: provides guaranteed lock management and queue limits to ensure mutually exclusive commands do not overlap on physical devices. **Status**: deferred for the first cut. The system relies solely on the basic command queue. If explicit global locks or backpressure limits become necessary, this module will be built out. # Protocol reference (/reference/protocol) The MDK Protocol is the contract that crosses every layer of the stack: Workers, `@tetherto/mdk-kernel`, and the Gateway all exchange the same envelope. This subsection holds the canonical specs. For the architectural narrative explaining how the protocol fits together, see [Architecture](/concepts/architecture#the-mdk-protocol). ## What's documented - **[Messages](/reference/protocol/messages)**: envelope schema, request/response examples, the full action catalogue, and the base command set. # Protocol messages (/reference/protocol/messages) Every MDK Protocol message uses the same envelope regardless of which layers are talking. This page is the canonical spec for that envelope, the full set of protocol actions, and the base command set every Worker must support. For the architectural narrative explaining when each action fires, see [Architecture](/concepts/architecture#the-mdk-protocol). ## Envelope ```json { "id": "uuid-v4", "version": "0.1.0", "type": "request | response | event", "action": "", "sender": "", "target": " | null", "deviceId": "string | null", "timestamp": 1711640000000, "payload": {} } ``` External consumers (UI or AI agents) only provide `deviceId`; the `target` Worker identity is internally resolved by `@tetherto/mdk-kernel`. A concrete request and response pair, end to end: ```json // request: Gateway asks @tetherto/mdk-kernel to reboot device wm001 { "id": "8d1c-e3a4", "version": "0.1.0", "type": "request", "action": "command.request", "sender": "appNode:fleet-api:1", "target": null, "deviceId": "wm001", "timestamp": 1711640000000, "payload": { "command": "reboot" } } // response: @tetherto/mdk-kernel relays the worker's terminal result { "id": "1f9b-77c2", "version": "0.1.0", "type": "response", "action": "command.result", "sender": "ork:kernel:tx-1", "target": "appNode:fleet-api:1", "deviceId": "wm001", "timestamp": 1711640002145, "payload": { "status": "SUCCESS", "elapsedMs": 2145 } } ``` ## Actions | Action | Type | Direction | Purpose | |---|---|---|---| | *(DHT presence)* | passive | Worker to DHT topic | Worker joins a known Hyperswarm topic; `@tetherto/mdk-kernel` detects its peer connection automatically | | `identity.request` | request | `@tetherto/mdk-kernel` to Worker | Requests the Worker's identity and managed devices | | `capability.request` | request | `@tetherto/mdk-kernel` to Worker | Asks the Worker to declare its full capability schema | | `state.pull` | request | `@tetherto/mdk-kernel` to Worker | Worker returns a snapshot of state-machine status (low cadence tick, e.g., 60s) | | `telemetry.pull` | request | `@tetherto/mdk-kernel` to Worker | Worker returns device metrics plus history (medium cadence tick, e.g., 10s) | | `command.request` | request | `@tetherto/mdk-kernel` to Worker | Resolves the Worker by `deviceId` and dispatches the command for execution | | `health.ping` | request | `@tetherto/mdk-kernel` to Worker | Liveness probe (high cadence tick, e.g., 5s) | ## Base command set The protocol standardizes a Base Command Set supported by every Worker: - `getConfig`: retrieve current device configuration - `setConfig`: update device configuration parameters - `health`: fetch detailed diagnostic health status from the device # Supported hardware (/reference/supported-hardware) ## Overview MDK integrates field hardware through Workers. Each Worker declares what it supports in its `mdk-contract.json`, and that contract is the single source of truth for coverage. Use this page to discover what Workers are supported. ## What MDK supports - **Miners**: For example, Bitmain Antminer, MicroBT Whatsminer - **Containers**: For example, Bitmain Antspace, Bitdeer - **Power meters**: For example, ABB, Satec - **Sensors**: For example, Seneca - **Mining pools**: Protocol integrations such as Ocean, F2Pool For the exact model lists, Worker packages, and per-Worker docs, see the generated catalogue: - [Full supported-hardware catalogue](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/supported-hardware.md) — generated from every `backend/workers/**/mdk-contract.json` ## Next steps - New to the moving parts? Read [terminology](/reference/glossary) (Kernel, Worker, manager, thing, mock) - Decide how to run the Worker service — [Deployment topologies](/concepts/deployment-topologies) - Run a miner Worker — [Run a miner Worker](/guides/miners) # UI Reference (/reference/ui) Complete API reference for the MDK UI packages: `@tetherto/mdk-react-devkit`, `@tetherto/mdk-react-adapter`, and `@tetherto/mdk-ui-foundation`. ## Quick Links | Section | Description | Count | |---------|-------------|-------| | [Components](/reference/ui/components) | React components for building UIs | 286 components | | [Hooks](/reference/ui/hooks) | React hooks for state and data | 113 hooks | | [Types](/reference/ui/types) | TypeScript type definitions | 774 types | | [Utilities](/reference/ui/utilities) | Helper functions and formatters | 149 utilities | ## Package Overview ### `@tetherto/mdk-react-devkit` The main UI component library. Provides: - Production-ready React components - Component-specific hooks - TypeScript types for all components ```tsx ``` ### `@tetherto/mdk-react-adapter` React bindings for the foundation layer. Provides: - Zustand store access hooks - Authentication hooks - Permission hooks - Data fetching hooks ```tsx ``` ### `@tetherto/mdk-ui-foundation` Framework-agnostic foundation layer. Provides: - Zustand stores - TanStack Query helpers - Utility functions - TypeScript types ```tsx ``` ## Getting Started 1. [Install the packages](/quickstart/wire-react) 2. Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` 3. Browse components by category or search for specific APIs # Components (/reference/ui/components) The `@tetherto/mdk-react-devkit` package provides production-ready React components organized by category. ## Prerequisites - Complete the [installation](/quickstart/wire-react) - Import the core styles in your app's entry point: ```tsx ``` ## Browse by category | Category | Description | |----------|-------------| | [Actions](/reference/ui/components/actions) | Buttons, action triggers, and export controls | | [Auth](/reference/ui/components/auth) | Authentication and sign-in components | | [Branding](/reference/ui/components/branding) | Logos, wordmarks, and brand elements | | [Cards](/reference/ui/components/cards) | Card containers and card-based layouts | | [Charts](/reference/ui/components/charts) | Data visualization and chart components | | [Dashboard](/reference/ui/components/dashboard) | Dashboard layouts and containers | | [Dashboards](/reference/ui/components/dashboards) | Pre-built dashboard compositions | | [Dialogs](/reference/ui/components/dialogs) | Modal dialogs and confirmation prompts | | [Display](/reference/ui/components/display) | Data display and formatting components | | [Features](/reference/ui/components/features) | Feature-specific composite components | | [Feedback](/reference/ui/components/feedback) | Alerts, toasts, and user feedback | | [Filters](/reference/ui/components/filters) | Filter controls and filter bars | | [Forms](/reference/ui/components/forms) | Form inputs, selects, and validation | | [Layout](/reference/ui/components/layout) | Page layouts, grids, and spacing | | [Media](/reference/ui/components/media) | Images, icons, and media display | | [Misc](/reference/ui/components/misc) | Utility and miscellaneous components | | [Monitoring](/reference/ui/components/monitoring) | System monitoring and status displays | | [Navigation](/reference/ui/components/navigation) | Sidebars, tabs, and navigation menus | | [Overlays](/reference/ui/components/overlays) | Popovers, tooltips, and overlay panels | | [Pages](/reference/ui/components/pages) | Full-page layouts and page shells | | [Settings](/reference/ui/components/settings) | Settings panels and preference controls | | [Tables](/reference/ui/components/tables) | Data tables and table utilities | | [Widgets](/reference/ui/components/widgets) | Dashboard widgets and data cards | ## Import pattern Components are imported from the package root: ```tsx ``` ## Styling Components use BEM-style CSS classes (e.g., `.mdk-button`, `.mdk-card__header`) for styling consistency. Every component forwards `className` to its root element. # Action Components (/reference/ui/components/actions) Components for triggering actions and user interactions. ## Prerequisites - Complete the [installation](/quickstart/wire-react) - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ## Button ```tsx ``` Primary action button with variants, sizes, loading state, icon placement, and full-width layout. Forwards refs and all native ` ) ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActionButton` [**`ActionButton`**](/reference/ui/components/actions/#actionbutton) component with confirmation popover or dialog `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `loading` | `boolean \| undefined` | | - | - | | `disabled` | `boolean \| undefined` | | - | - | | `className` | `string \| undefined` | | - | - | | `variant` | `TActionButtonVariant \| undefined` | | - | - | | `confirmation` | `ActionButtonConfirmation` | ✓ | - | - | | `mode` | `"dialog" \| "popover" \| undefined` | | - | Confirmation mode: popover (inline) or dialog (modal). Default: popover | ### `Button` Primary action button. Supports loading state with spinner, icon placement, variants, sizes, and full-width layout. Forwards refs and all native button attributes. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `loading` | `boolean` | | - | Show a spinner instead of the content and disable the button. | | `fullWidth` | `boolean` | | - | Make the button stretch to fill its container. | | `icon` | `React.ReactNode` | | - | Icon node rendered alongside `children`. | | `variant` | `ButtonVariant` | | - | Visual variant (e.g. `primary`, `secondary`, `ghost`). | | `contentClassName` | `string` | | - | Class names applied to the inner content wrapper. | | `iconPosition` | `ButtonIconPosition` | | - | Icon placement relative to children. | | `size` | `ComponentSize` | | - | Size token (`sm`, `md`, `lg`). | ### `StatsExport` Dropdown button that triggers asynchronous CSV or JSON export. Shows a spinner while the corresponding handler is awaited. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `showLabel` | `boolean \| undefined` | | - | - | | `disabled` | `boolean \| undefined` | | - | - | | `onCsvExport` | `() => Promise` | ✓ | - | - | | `onJsonExport` | `() => Promise` | ✓ | - | - | # Auth Components (/reference/ui/components/auth) Components for authentication flows and user sign-in. ## Prerequisites - Complete the [installation](/quickstart/wire-react) - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `RequireAuth` Route guard that reads the session token from the headless `authStore` (via `useAuth`) and renders the children only when a token is present. Otherwise it renders `fallback` — typically `` from `react-router`. Router-agnostic by design. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `React.ReactNode` | ✓ | - | Rendered when a token is present. | | `fallback` | `React.ReactNode` | ✓ | - | Rendered when no token is present — typically ``. | | `rememberPath` | `boolean \| undefined` | | - | When true (default), the current location is persisted to sessionStorage before rendering the fallback so the sign-in flow can return there. | ### `SignInGoogleButton` One-click Google OAuth sign-in trigger. Defaults to a full-page redirect to `${oauthBaseUrl}/oauth/google`, mirroring the production MOS flow. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | ✓ | - | Icon node rendered alongside `children`. | | `loading` | `boolean \| undefined` | ✓ | - | Show a spinner instead of the content and disable the button. | | `variant` | `ButtonVariant \| undefined` | ✓ | - | Visual variant (e.g. `primary`, `secondary`, `ghost`). | | `size` | `ComponentSize \| undefined` | ✓ | - | Size token (`sm`, `md`, `lg`). | | `fullWidth` | `boolean \| undefined` | ✓ | - | Make the button stretch to fill its container. | | `contentClassName` | `string \| undefined` | ✓ | - | Class names applied to the inner content wrapper. | | `iconPosition` | `ButtonIconPosition \| undefined` | ✓ | - | Icon placement relative to children. | | `oauthBaseUrl` | `string` | ✓ | - | Base URL of the OAuth backend (no trailing slash). Click navigates to `${oauthBaseUrl}/oauth/google`. | | `label` | `string \| undefined` | | - | Override the visible button label. | | `onClick` | `(() => void) \| undefined` | | - | Override the click behaviour entirely. When set, `oauthBaseUrl` is ignored. | # Branding Components (/reference/ui/components/branding) Components for brand identity and visual consistency. ## Prerequisites - Complete the [installation](/quickstart/wire-react) - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `MdkWordmark` MDK wordmark — the canonical brand lockup, rendered as inline SVG so it tints to `currentColor`. Use this in [``](/reference/ui/components/navigation/#appheader) (via the `logo` slot) or anywhere else the brand should appear. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `size` | `MdkWordmarkSize \| undefined` | | - | Visual size of the wordmark. `sm` ≈ 24px tall, `md` ≈ 32px, `lg` ≈ 64px. | | `className` | `string \| undefined` | | - | Optional class hook on the outer ``. | | `title` | `string \| undefined` | | - | Accessible label. Defaults to "MDK". | # Card Components (/reference/ui/components/cards) Card components for grouping and presenting content. ## Prerequisites - Complete the [installation](/quickstart/wire-react) - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActiveIncidentsCard` Summary card displaying a list of active incidents/alerts with severity indicators, loading skeleton, and empty state. Rows are virtualized via `@tanstack/react-virtual` so the card stays responsive with thousands of incidents. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string` | | - | - | | `isLoading` | `boolean` | | - | - | | `className` | `string` | | - | - | | `skeletonRows` | `number` | | - | - | | `emptyMessage` | `string` | | - | - | | `items` | `TIncidentRowProps[]` | | - | - | | `onItemClick` | `(id: string) => void` | | - | - | ### `CabinetDetailCard` Read-only LV cabinet detail: powermeter readings, the root plus per-position temperature readings (severity-coloured, with an offline marker), and the active-warnings timeline. Presentational — shape the rows with [`useCabinetDetail`](/reference/ui/hooks/cards/#usecabinetdetail). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | ✓ | - | Cabinet display title (`LV Cabinet 1` / transformer title). | | `powerMeters` | `CabinetReadingRow[]` | ✓ | - | Non-root powermeter reading rows. | | `rootTempSensor` | `CabinetReadingRow \| undefined` | | - | The cabinet-root temperature reading, when present. | | `tempSensors` | `CabinetReadingRow[]` | ✓ | - | Non-root temperature sensor reading rows. | | `alarmsDataItems` | `TimelineItemData[]` | ✓ | - | Active-warnings timeline items. | | `onNavigate` | `((path: string) => void) \| undefined` | | - | Router navigate used by warning rows to deep-link into the alert. | | `isLoading` | `boolean \| undefined` | | - | Shows a spinner while the cabinet snapshot is loading. | ### `MetricCard` Compact card displaying a labelled metric value with optional highlight and transparency states. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string` | ✓ | - | - | | `unit` | `string` | ✓ | - | - | | `value` | `string \| number \| null` | ✓ | - | - | | `bgColor` | `string \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | ✓ | - | - | | `noMinWidth` | `boolean \| undefined` | ✓ | - | - | | `isValueMedium` | `boolean \| undefined` | ✓ | - | - | | `isHighlighted` | `boolean \| undefined` | ✓ | - | - | | `showDashForZero` | `boolean \| undefined` | ✓ | - | - | | `isTransparentColor` | `boolean \| undefined` | ✓ | - | - | ### `MiningPoolsPanel` Dashboard card that lists configured mining pools — one row per pool, with revenue, hash rate, and an optional "Show details" action. Pure presentation: the row data + click handler come from props, shaped upstream by `usePoolRows` (or any caller producing `MiningPoolRow`s). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string` | | - | Override the card title — defaults to `Mining Pools`. | | `hideHeader` | `boolean` | | - | Hide the title row entirely. | | `isLoading` | `boolean` | | - | Loading state — renders skeleton rows. | | `skeletonRows` | `number` | | - | Number of skeleton rows to show while loading. | | `emptyMessage` | `string` | | - | Message shown when `rows` is empty. | | `rows` | `MiningPoolRow[]` | | - | Pool rows, in display order. | | `onShowDetails` | `(row: MiningPoolRow) => void` | | - | Called when the user clicks the per-row "Show details" button. | | `className` | `string` | | - | Extra className for the root. | ### `PoolDetailsCard` Compact key/value card for displaying pool metadata (URL, fee, worker count, etc.). Empty list renders a "No data available" placeholder. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | ✓ | - | - | | `underline` | `boolean \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | ✓ | - | - | | `details` | `PoolDetailItem[]` | ✓ | - | - | ### `PoolDetailsPopover` [**`Button`**](/reference/ui/components/actions/#button)-triggered popover that displays a pool's key/value details (URL, fee, worker count, status, …) inside a Radix `Dialog`. Wraps [`PoolDetailsCard`](/reference/ui/components/cards/#pooldetailscard) so the read-out matches the embedded card variant. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string \| undefined` | ✓ | - | - | | `description` | `string \| undefined` | ✓ | - | - | | `disabled` | `boolean \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | ✓ | - | - | | `triggerLabel` | `string \| undefined` | ✓ | - | - | | `details` | `PoolDetailItem[]` | ✓ | - | - | # Chart Components (/reference/ui/components/charts) Components for data visualization and charting. ## Prerequisites - Complete the installation - Complete the installation ```bash # Clone the MDK UI monorepo (adjust the URL to your fork if needed) git clone https://github.com/tetherto/mdk.git cd mdk/ui # Install dependencies and build packages (npm workspaces) npm install npm run build ``` - Add the dependency to your app's `package.json` ```json { "dependencies": { "@tetherto/mdk-react-devkit": "*", "@tetherto/mdk-react-adapter": "*", "@tetherto/mdk-ui-foundation": "*" } } ``` > **Coming soon** — npm packages are not yet published. Use the monorepo setup for now. ```bash npm install \ @tetherto/mdk-react-devkit \ @tetherto/mdk-react-adapter \ @tetherto/mdk-ui-foundation ``` Run `npm install` from the `mdk/ui` workspace root after your app is under `apps/` so npm links workspace packages. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ## Chart container ```tsx ``` A layout wrapper for charts that provides a title/header row, interactive legend, range selector, highlighted value display, loading/empty states, and a stats footer. ## Notes - `minMaxAvg` and `timeRange` are only rendered when the chart is not loading or empty. ## Example ```tsx /** * Runnable example for ChartContainer. */ const RANGE_OPTIONS = [ { label: '1H', value: '1h' }, { label: '24H', value: '24h' }, { label: '7D', value: '7d' }, ] const LEGEND_DATA = [ { label: 'Pool A', color: '#59E8E8' }, { label: 'Pool B', color: '#FF9500' }, ] const MockChart = () => (
Chart content
) const [range, setRange] = useState('24h') return (
) } ``` ## Related API - [**`useChartDataCheck`**](/reference/ui/hooks/charts/#usechartdatacheck) @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActualEbitdaCard` Stat card summarising the realised EBITDA for the selected reporting window vs the prior period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `AreaChart` Presentational Chart.js area chart (Line with fill). Data must be provided via props; this component does no fetching of its own. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `ChartData<"line", (number \| Point \| null)[], unknown>` | ✓ | - | Chart data - required, provided by parent | | `options` | `_DeepPartialObject & ElementChartOptions<"line"> & PluginChartOptions<"line"> & DatasetChartOptions<"line"> & ScaleChartOptions<"line"> & LineControllerChartOptions> \| undefined` | | - | Chart.js options - merged with defaults | | `tooltip` | `ChartTooltipConfig \| undefined` | | - | Custom HTML tooltip configuration. When provided, replaces the default Chart.js tooltip. | | `height` | `number \| undefined` | | - | Chart height in pixels | | `className` | `string \| undefined` | | - | - | ### `AverageDowntimeChart` Stacked bar chart of average downtime (curtailment vs operational issues). Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`BarChart`](/reference/ui/components/charts/#barchart); pass period labels and rate arrays via `data`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | | - | - | | `unit` | `string` | | - | - | | `height` | `number` | | - | - | | `barWidth` | `number` | | - | - | | `className` | `string` | | - | - | | `isLoading` | `boolean` | | - | - | | `emptyMessage` | `string` | | - | - | | `data` | `AverageDowntimeChartData` | | - | - | | `yTicksFormatter` | `(value: number) => string` | | - | Formats Y-axis ticks, tooltips, and bar data labels (values are 0–1 rates). | | `showDataLabels` | `boolean` | | - | - | ### `AvgAllInCostChart` Avg All-in [**`Cost`**](/reference/ui/components/dashboards/#cost) - revenue vs cost ($/MWh) bar chart over time. Renders the OSS `SiteEnergyVsCostChart`. The revenue/cost time-series isn't carried by the cost-summary response, so consumers feed it through as a separate prop (the OSS app sources it from `useAvgAllInPowerCostData`). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `readonly AvgAllInCostDataPoint[] \| undefined` | | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `BarChart` Presentational Chart.js bar chart. Data must be pre-aggregated; use grouped or stacked categories via `datasets`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `any` | ✓ | - | Chart data - required, provided by parent. Use `as any` for mixed bar+line datasets. | | `options` | `_DeepPartialObject & ElementChartOptions<"bar"> & PluginChartOptions<"bar"> & DatasetChartOptions<"bar"> & ScaleChartOptions<"bar"> & BarControllerChartOptions> \| undefined` | | - | Chart.js options - merged with defaults | | `isStacked` | `boolean \| undefined` | | - | Stack bars on top of each other | | `isHorizontal` | `boolean \| undefined` | | - | Render bars horizontally (indexAxis: 'y') | | `formatYLabel` | `((value: number) => string) \| undefined` | | - | Format Y-axis tick labels | | `showLegend` | `boolean \| undefined` | | - | Show built-in Chart.js legend (default: true) | | `legendPosition` | `Position \| undefined` | | - | Position of the legend (default: 'top') | | `legendAlign` | `FlexAlign \| undefined` | | - | Alignment of the legend labels within their position (default: 'start') | | `showDataLabels` | `boolean \| undefined` | | - | Show values above each bar | | `formatDataLabel` | `((value: number) => string) \| undefined` | | - | Format data label values (default: round to nearest integer) | | `tooltip` | `ChartTooltipConfig \| undefined` | | - | Custom HTML tooltip configuration. When provided, replaces the default Chart.js tooltip. | | `height` | `number \| undefined` | | - | Chart height in pixels | | `className` | `string \| undefined` | | - | - | ### `BitcoinPriceCard` Stat card showing the BTC reference price used by the reporting view with currency and timestamp. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `BitcoinProducedCard` Stat card summarising the bitcoin produced during the reporting window with delta to prior period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `BitcoinProducedChart` Time-series chart of bitcoin produced per day across the selected reporting window. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `hasAllZeros` | `boolean \| undefined` | | - | - | | `height` | `number \| undefined` | | - | - | ### `BitcoinProductionCostCard` Stat card showing the average cost in USD to produce one bitcoin during the reporting window. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `ChartContainer` Standard chrome for charts: title, optional highlighted value, legend with toggle, range selector (radio cards), loading / empty states, and a footer for min/max/avg stats. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string \| undefined` | | - | - | | `titleExtra` | `React.ReactNode` | | - | Optional node rendered immediately after the title text (e.g. an info tooltip). Only shown when `title` is set and `header` is not. Additive - omit it and the title renders exactly as before. | | `header` | `React.ReactNode` | | - | - | | `headerAction` | `React.ReactNode` | | - | Optional action rendered on the right side of the header row (e.g. an expand/fullscreen toggle). Sits alongside the range selector when both are present. Purely additive - omit it and the header renders exactly as before. | | `legendData` | `LegendItem[] \| undefined` | | - | - | | `highlightedValue` | `HighlightedValueProps \| undefined` | | - | - | | `rangeSelector` | `RangeSelectorProps \| undefined` | | - | - | | `loading` | `boolean \| undefined` | | - | - | | `empty` | `boolean \| undefined` | | - | - | | `emptyMessage` | `string \| undefined` | | - | - | | `minMaxAvg` | `Partial<{ min: string; max: string; avg: string; }> \| undefined` | | - | - | | `timeRange` | `string \| undefined` | | - | - | | `footer` | `React.ReactNode` | | - | - | | `footerClassName` | `string \| undefined` | | - | - | | `className` | `string \| undefined` | | - | - | | `children` | `React.ReactNode` | ✓ | - | - | | `onToggleDataset` | `((index: number) => void) \| undefined` | | - | - | ### `ChartExpandAction` Expand / collapse toggle rendered in a dashboard chart card's header. Swaps between a maximize and a minimize glyph based on `isExpanded`. `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isExpanded` | `boolean` | ✓ | - | Whether the parent chart is currently expanded to full width. | | `onToggle` | `VoidFunction \| undefined` | | - | Toggles the expanded state. | ### `ChartStatsFooter` [**`ChartStatsFooter`**](/reference/ui/components/charts/#chartstatsfooter) - Displays Min/Max/Avg values and optional stats grid below a chart `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `minMaxAvg` | `Partial<{ min: string; max: string; avg: string; }>` | | - | Min/Max/Avg values row | | `stats` | `ChartStatsFooterItem[]` | | - | Additional stats displayed in a columnar grid | | `statsPerColumn` | `number` | | - | Number of stat items per column (default: 1) | | `secondaryLabel` | `SecondaryLabel` | | - | Secondary label displayed below stats | | `className` | `string` | | - | Custom class name | ### `CostCharts` Convenience wrapper that renders the three cost-page charts in declaration order. Pages that need bespoke layouts (e.g. [`CostContent`](/reference/ui/components/dashboards/#costcontent)'s 2x2 [**`Mosaic`**](/reference/ui/components/layout/#mosaic)) compose the individual chart components directly instead. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `costLog` | `readonly CostTimeSeriesEntry[]` | ✓ | - | - | | `btcPriceLog` | `readonly BtcPriceTimeSeriesEntry[]` | ✓ | - | - | | `totals` | `CostSummaryMonetaryTotals \| null` | ✓ | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `avgAllInCostData` | `readonly AvgAllInCostDataPoint[] \| undefined` | | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `DetailLegend` [**`DetailLegend`**](/reference/ui/components/charts/#detaillegend) - Enhanced chart legend with current values and percentage change indicators `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `items` | `DetailLegendItem[]` | ✓ | - | Legend items to display | | `onToggle` | `((label: string, index: number) => void) \| undefined` | | - | Callback when a legend item is toggled | | `className` | `string \| undefined` | | - | Custom class name | ### `DoughnutChart` [**`DoughnutChart`**](/reference/ui/components/charts/#doughnutchart) – Presentational Chart.js doughnut chart with custom HTML legend matching the MDK design. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `DoughnutChartDataset[]` | ✓ | - | Array of labelled slices | | `unit` | `string \| undefined` | | - | Unit suffix shown in tooltips | | `options` | `_DeepPartialObject & ElementChartOptions<"doughnut"> & PluginChartOptions<"doughnut"> & DatasetChartOptions<"doughnut"> & ScaleChartOptions<"doughnut"> & DoughnutControllerChartOptions> \| undefined` | | - | Chart.js options – merged with defaults | | `cutout` | `string \| undefined` | | - | Doughnut cutout percentage (default: '75%') | | `borderWidth` | `number \| undefined` | | - | Border width between segments (default: 4) | | `height` | `number \| undefined` | | - | Chart height in pixels | | `legendPosition` | `Position \| undefined` | | - | Where to place the legend relative to the chart (default: 'top') | | `tooltip` | `ChartTooltipConfig \| undefined` | | - | Custom HTML tooltip configuration. When provided, replaces the default doughnut tooltip (which shows label, value with unit, and percentage). Use `valueFormatter` to replicate the percentage display if needed. | | `formatValue` | `((value: number) => string) \| undefined` | | - | Formats slice values in the built-in legend and default tooltip (default: raw number). | | `className` | `string \| undefined` | | - | - | ### `Ebitda` Top-level EBITDA section of the reporting view — pulls together metric cards, charts, and tables. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EbitdaDisplayMetrics \| null` | ✓ | - | - | | `ebitdaChartInput` | `ToBarChartDataInput \| null` | ✓ | - | - | | `btcProducedChartInput` | `ToBarChartDataInput \| null` | ✓ | - | - | | `hasBtcProducedAllZeros` | `boolean` | ✓ | - | - | | `showEbitdaBarChart` | `boolean` | ✓ | - | - | | `currentBTCPrice` | `number` | ✓ | - | - | | `datePicker` | `React.ReactElement>` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `errors` | `string[] \| undefined` | | - | - | | `hasDateSelection` | `boolean` | ✓ | - | When false, show the "select a period" hint instead of empty data. | | `setCostHref` | `string \| undefined` | | - | Optional URL for the "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" control (hidden when omitted). | ### `EbitdaCharts` Chart panel inside the EBITDA section visualising revenue, cost, and EBITDA over time. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `showEbitdaBarChart` | `boolean` | ✓ | - | - | | `ebitdaChartData` | `BarChartDataResult` | ✓ | - | - | | `btcDisplayData` | `BarChartDataResult` | ✓ | - | - | | `isLoading` | `boolean` | ✓ | - | - | | `hasBtcProducedAllZeros` | `boolean` | ✓ | - | - | ### `EbitdaHodlCard` Stat card projecting EBITDA assuming all produced bitcoin is held instead of sold. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | | `currentBTCPrice` | `number` | ✓ | - | - | ### `EbitdaMetrics` Row of summary metric cards across the top of the EBITDA section (actual, hodl, selling, cost). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EbitdaDisplayMetrics` | ✓ | - | - | | `currentBTCPrice` | `number` | ✓ | - | - | ### `EbitdaSellingCard` Stat card projecting EBITDA assuming all produced bitcoin is sold at the daily reference price. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `EnergyBalance` Full energy balance view with tabbed revenue and cost sections, charts, and metric cards. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `viewModel` | `EnergyBalanceViewModel` | ✓ | - | - | | `onTabChange` | `(tab: EnergyBalanceTab) => void` | ✓ | - | - | | `onRevenueDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `onCostDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `isDemoMode` | `boolean \| undefined` | | - | - | | `timeframeControls` | `React.ReactNode` | | - | Slot for timeframe / date-range controls rendered by the host app. | | `setCostHref` | `string \| undefined` | | - | Optional URL for the "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" control (hidden when omitted). | ### `EnergyBalanceCostCharts` Layout container for the energy cost tab charts: revenue-vs-cost bar chart and power line chart. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `costChartData` | `BarChartDataResult` | ✓ | - | - | | `btcUnit` | `string \| null` | ✓ | - | - | | `powerChartInput` | `ThresholdLineChartInput` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `showCostBarChart` | `boolean` | ✓ | - | Show the revenue-vs-cost bar chart only for non-daily periods. | | `periodType` | `PeriodType` | ✓ | - | - | ### `EnergyBalanceCostMetrics` Grid of stat cards summarising energy cost metrics for the selected period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EnergyCostMetrics` | ✓ | - | - | ### `EnergyBalancePowerChart` Line chart visualising power consumption against threshold for the energy balance view. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `height` | `number \| undefined` | | - | - | | `fillHeight` | `boolean \| undefined` | | - | - | | `periodType` | `PeriodType` | ✓ | - | - | | `chartInput` | `ThresholdLineChartInput` | ✓ | - | - | ### `EnergyBalanceRevenueCharts` [**`Mosaic`**](/reference/ui/components/layout/#mosaic) layout of revenue, downtime, and power charts for the energy balance revenue tab. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `revenueChartData` | `BarChartDataResult` | ✓ | - | - | | `averageDowntimeData` | `AverageDowntimeChartData` | ✓ | - | - | | `powerChartInput` | `ThresholdLineChartInput` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `periodType` | `PeriodType` | ✓ | - | - | | `revenueMetrics` | `EnergyRevenueMetrics` | ✓ | - | - | ### `EnergyBalanceRevenueMetrics` Grid of stat cards summarising energy revenue metrics for the selected period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EnergyRevenueMetrics` | ✓ | - | - | ### `EnergyCostChart` Bar chart comparing site revenue vs cost per MWh, with USD/BTC currency toggle. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `btcUnit` | `string \| null` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `height` | `number \| undefined` | | - | - | ### `EnergyMetricCard` Stat card for a single energy balance metric. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `name` | `string` | ✓ | - | - | | `value` | `number` | ✓ | - | - | | `unit` | `string` | ✓ | - | - | | `fallback` | `string \| undefined` | | - | - | ### `EnergyReportMinerTypeView` Energy report — power consumption grouped by miner model (latest day in range). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isLoading` | `boolean \| undefined` | | - | - | | `containers` | `Container[] \| undefined` | | - | - | | `groupedConsumption` | `MetricsConsumptionGroupedResponse \| undefined` | | - | - | | `onTimeFrameChange` | `((start: Date, end: Date) => void) \| undefined` | | - | - | ### `EnergyReportMinerUnitView` Energy report — power consumption grouped by mining unit / container. `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isLoading` | `boolean \| undefined` | | - | - | | `containers` | `Container[] \| undefined` | | - | - | | `groupedConsumption` | `MetricsConsumptionGroupedResponse \| undefined` | | - | - | | `onTimeFrameChange` | `((start: Date, end: Date) => void) \| undefined` | | - | - | ### `EnergyReportSiteView` Energy report site tab — power trend, power-mode table, and per–mining-unit activity cards. `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `consumptionError` | `unknown` | | - | - | | `tailLogLoading` | `boolean \| undefined` | | - | - | | `containersLoading` | `boolean \| undefined` | | - | - | | `consumptionLoading` | `boolean \| undefined` | | - | - | | `consumptionFetching` | `boolean \| undefined` | | - | - | | `nominalConfigLoading` | `boolean \| undefined` | | - | - | | `containers` | `EnergyReportContainer[] \| undefined` | | - | - | | `tailLog` | `EnergyReportTailLogItem[][] \| undefined` | | - | - | | `nominalPowerAvailabilityMw` | `number \| null \| undefined` | | - | - | | `consumptionLog` | `MetricsConsumptionLogEntry[] \| undefined` | | - | - | | `snapshotLoading` | `boolean \| undefined` | | - | - | | `onRefetchSnapshot` | `VoidFunction \| undefined` | | - | - | | `dateRange` | `EnergyReportDateRange` | ✓ | - | - | | `onDateRangeChange` | `((range: EnergyReportDateRange) => void) \| undefined` | | - | - | ### `EnergyRevenueChart` Bar chart showing site energy revenue per MWh, with USD/BTC currency toggle. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `height` | `number \| undefined` | | - | - | ### `GaugeChart` [**`GaugeChart`**](/reference/ui/components/charts/#gaugechart) - Presentational gauge / speedometer chart. Implementation note: this component used to wrap the `react-gauge-chart` NPM package, but that package is published only as CommonJS with a broken `module` field that points at the same CJS file. That made it crash under ESM bundlers that don't add a `__esModule ? .default : module` interop shim (Webpack 4, raw esbuild, certain SSR setups, etc.) with React's "Element type is invalid" error. We replaced it with a pure-SVG internal implementation (see `./gauge-svg.tsx`) so the component is bundler- and runtime-agnostic and has zero third-party runtime dependencies. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `percent` | `number` | ✓ | - | Value between 0 and 1 (e.g. 0.75 = 75%). Values outside the range are clamped. | | `colors` | `string[] \| undefined` | | - | Arc colours in HEX format. | | `arcWidth` | `number \| undefined` | | - | Arc thickness as a fraction of the gauge radius (0–1). | | `nrOfLevels` | `number \| undefined` | | - | Number of arc segments. Ignored when `arcsLength` is provided. | | `arcsLength` | `number[] \| undefined` | | - | Custom arc-segment proportions (auto-normalised), overriding `nrOfLevels`. e.g. `[0.7, 0.3]` for a progress-style gauge whose first arc is the fill. | | `needleColor` | `string \| undefined` | | - | Needle + hub colour. | | `hideNeedle` | `boolean \| undefined` | | - | Hide the needle + hub (e.g. for a progress-style gauge). | | `formatTextValue` | `((percent: number) => string) \| undefined` | | - | Format the centre label from the clamped fraction (0–1). | | `hideText` | `boolean \| undefined` | | - | Hide the percentage text rendered inside the gauge. | | `id` | `string \| undefined` | | - | Stable id used for the gauge's accessibility labels. | | `height` | `string \| number \| undefined` | | - | Chart height in pixels or any CSS length (e.g. `'200px'` or `'50%'`). | | `maxWidth` | `number \| undefined` | | - | Maximum width in pixels. | | `className` | `string \| undefined` | | - | - | ### `Hashrate` Top-level hashrate reporting section - composes the site / miner-type / mining-unit drilldowns into a tabbed shell. Each tab fetches independently because the three views use different `groupBy` axes; the composite stitches them via per-tab prop bags. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `defaultTab` | `HashrateTabValue \| undefined` | | - | Tab selected on first render. Defaults to the Site View. | | `siteView` | `HashrateSiteViewProps \| undefined` | | - | Props forwarded to the Site View tab. | | `minerTypeView` | `HashrateMinerTypeViewProps \| undefined` | | - | Props forwarded to the Miner Type View tab. | | `miningUnitView` | `HashrateMiningUnitViewProps \| undefined` | | - | Props forwarded to the Mining Unit View tab. | ### `HashrateMinerTypeView` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) drilldown grouped by miner model - bar chart of the latest hashrate per miner type, with an optional multi-select filter. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `log` | `HashrateGroupedLog \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by miner type (groupBy=miner). | | `isLoading` | `boolean \| undefined` | | - | - | | `dateRange` | `HashrateDateRange \| undefined` | | - | - | | `onDateRangeChange` | `((range: HashrateDateRange) => void) \| undefined` | | - | - | | `onReset` | `VoidFunction \| undefined` | | - | - | ### `HashrateMiningUnitView` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) drilldown grouped by mining unit / container - bar chart of the latest hashrate per container with an optional multi-select filter. Drops BE-leaked rollup keys (`group-N`, `maintenance`) via the utils layer. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `log` | `HashrateGroupedLog \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by container / mining unit (groupBy=container). | | `isLoading` | `boolean \| undefined` | | - | - | | `dateRange` | `HashrateDateRange \| undefined` | | - | - | | `onDateRangeChange` | `((range: HashrateDateRange) => void) \| undefined` | | - | - | | `onReset` | `VoidFunction \| undefined` | | - | - | ### `HashrateSiteView` Site-level hashrate trend - aggregates hashrate across the whole site for the selected date range, with an optional miner-type filter that scopes the sum to a subset. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `log` | `HashrateGroupedLog \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by miner type. | | `isLoading` | `boolean \| undefined` | | - | Loading state - drives the chart spinner. | | `dateRange` | `HashrateDateRange \| undefined` | | - | Selected date range used by the host to drive the query. | | `onDateRangeChange` | `((range: HashrateDateRange) => void) \| undefined` | | - | Fires when the user picks a new range from the [**`DateRangePicker`**](/reference/ui/components/forms/#daterangepicker). | | `onReset` | `VoidFunction \| undefined` | | - | Optional reset handler shown as a "Reset" button next to the date picker. | ### `Heatmap` [**`Heatmap`**](/reference/ui/components/charts/#heatmap) — a generic grid of value-coloured cells on a low→high gradient. Presentational and domain-agnostic: pass a row-major matrix of cells and an optional `[min, max]` range (auto-derived otherwise). Use `renderCell` to overlay domain content (socket borders, selection, tooltips) without forking the primitive — the grid still owns each cell's background colour. Pair with [`HeatmapLegend`](/reference/ui/components/charts/#heatmaplegend) for a gradient scale. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `HeatmapCell[][]` | ✓ | - | Rows of cells (row-major). Rows may be ragged. | | `min` | `number \| undefined` | | - | Range floor; auto-derived from the finite values when omitted. | | `max` | `number \| undefined` | | - | Range ceiling; auto-derived from the finite values when omitted. | | `colors` | `readonly string[] \| undefined` | | - | Gradient stops low→high. Defaults to the cold→hot `HEATMAP_GRADIENT`. | | `emptyColor` | `string \| undefined` | | - | Colour used for `null` cells. | | `showValues` | `boolean \| undefined` | | - | Render each cell's value/label as text. | | `renderCell` | `((cell: HeatmapCell, context: HeatmapCellContext) => React.ReactNode) \| undefined` | | - | Override the cell's inner content — e.g. to overlay socket borders, selection, or tooltips for a PDU grid. The primitive still owns the cell's background colour (passed via `context.color`). | | `ariaLabel` | `string \| undefined` | | - | Accessible label for the grid. | | `className` | `string \| undefined` | | - | - | ### `HeatmapLegend` [**`HeatmapLegend`**](/reference/ui/components/charts/#heatmaplegend) — a gradient bar with low/high scale labels, matching the gradient used by [`Heatmap`](/reference/ui/components/charts/#heatmap). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `min` | `string \| number` | ✓ | - | Value (or pre-formatted label) at the low end of the scale. | | `max` | `string \| number` | ✓ | - | Value (or pre-formatted label) at the high end of the scale. | | `unit` | `string \| undefined` | | - | Unit suffix appended to `min`/`max`. | | `label` | `string \| undefined` | | - | Heading above the gradient bar (e.g. "Temperature"). | | `colors` | `readonly string[] \| undefined` | | - | Gradient stops low→high. Defaults to `HEATMAP_GRADIENT`. | | `className` | `string \| undefined` | | - | - | ### `LineChart` Customisable Chart.js line chart with built-in zoom, tooltip, and legend. Data is passed via props; the component does no fetching. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartRef` | `React.MutableRefObject \| undefined` | | - | Mutable ref to hold the LightWeightCharts reference | | `data` | `LineChartData` | ✓ | - | Data of the chart | | `yTicksFormatter` | `((value: number) => string) \| undefined` | | - | Callback to format ticks on y axis. If `priceFormatter` is given. It would be used instead. | | `customLabel` | `string \| undefined` | | - | TODO: Doc | | `priceFormatter` | `((value: number) => string) \| undefined` | | - | Callback to format ticks on y axis. | | `roundPrecision` | `number \| undefined` | | - | The number of decimals to show | | `timeline` | `string \| undefined` | | - | TODO: DOC | | `fixedTimezone` | `string \| undefined` | | - | Applies offset if provided, otherwise timestamps are assumed to already be in local time. Otherwise, use browser's current timezone offset for consistent time display | | `shouldResetZoom` | `boolean \| undefined` | | - | Wether to Reset Zoom | | `skipRound` | `boolean \| undefined` | | - | Prevent rounding of values | | `skipMinWidth` | `boolean \| undefined` | | - | Do not enforce a min width | | `fadedBackground` | `boolean \| undefined` | | - | Use a faded background | | `backgroundColor` | `string \| undefined` | | - | Background color of the chart | | `customDateFormat` | `string \| undefined` | | - | Custom date format | | `verticalLineLabelVisible` | `boolean \| undefined` | | - | Show vertical line at mouse position | | `horizontalLineLabelVisible` | `boolean \| undefined` | | - | Show horizontal line at mouse position | | `showDateInTooltip` | `boolean \| undefined` | | - | Show date line in tooltip | | `disableAutoRange` | `boolean \| undefined` | | - | Disable automatically determining range | | `uniformDistribution` | `boolean \| undefined` | | - | Changes horizontal scale marks generation. With this flag equal to true, marks of the same weight are either all drawn or none are drawn at all. | | `unit` | `string \| undefined` | | - | The unit to display with values | | `beginAtZero` | `boolean \| undefined` | | - | Starts the value axis at 0 | | `showPointMarkers` | `boolean \| undefined` | | - | Show a marker on the line | | `height` | `number \| undefined` | | - | Controls the height of the chart. Default: 240 | ### `LineChartCard` Composable line-chart card with title, timeline range selector, legend (basic or detailed), error boundary, and an optional min/max/avg footer. Accepts either pre-shaped `data` or `rawData` + a `dataAdapter` callback so upstream domain components can keep their data wrangling local. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | Pre-adapted chart data (use this OR rawData+dataAdapter) | | `rawData` | `unknown` | | - | Raw data to be transformed by dataAdapter | | `dataAdapter` | `(data: unknown) => LineChartCardData` | | - | Adapter to transform rawData into LineChartCardData | | `timelineOptions` | `TimelineOption[]` | | - | Timeline range selector options | | `timeline` | `string` | | - | Controlled timeline value | | `defaultTimeline` | `string` | | - | Default timeline when uncontrolled | | `onTimelineChange` | `(timeline: string) => void` | | - | Callback when timeline changes | | `title` | `string` | | - | Chart title | | `detailLegends` | `boolean` | | - | Show detail legends with current values | | `isLoading` | `boolean` | | - | Loading state | | `shouldResetZoom` | `boolean` | | - | Whether to reset zoom on timeline change (default: true) | | `chartProps` | `Partial` | | - | Pass-through props to the core [**`LineChart`**](/reference/ui/components/charts/#linechart) | | `chartRef` | `React.MutableRefObject` | | - | Ref to the lightweight-charts IChartApi | | `minHeight` | `string \| number` | | - | - | | `className` | `string` | | - | Custom class name | | `headerAction` | `React.ReactNode` | | - | Optional action rendered on the right of the card header (e.g. an expand toggle). Passed straight through to [`ChartContainer`](/reference/ui/components/charts/#chartcontainer). Additive - omit it and the card header is unchanged. | | `titleExtra` | `React.ReactNode` | | - | Optional node rendered next to the title (e.g. an info tooltip). Passed straight through to [`ChartContainer`](/reference/ui/components/charts/#chartcontainer). Additive. | ### `MinMaxAvg` Min / Max / Avg summary row with consistent MDK label and value styling. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `min` | `string \| undefined` | ✓ | - | - | | `max` | `string \| undefined` | ✓ | - | - | | `avg` | `string \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | | - | - | ### `MonthlyEbitdaChart` Bar chart comparing EBITDA across the most recent months for trend visualisation. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `height` | `number \| undefined` | | - | - | ### `OperationalHashrateChart` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) trend card for the operational dashboard. Renders the site hashrate over time (TH/s) with an optional nominal reference line, plus an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationalMinersStatusChart` Miners-status card for the operational dashboard. Renders a stacked daily breakdown of miner states (online / error / offline / sleep / maintenance) with an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `MinersStatusChartData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationalPowerConsumptionChart` Power-consumption trend card for the operational dashboard. Renders site power draw over time (MW) with an optional power-availability reference line, plus an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationalSiteEfficiencyChart` Site-efficiency trend card for the operational dashboard. Renders measured site efficiency over time (W/TH/s) with an optional nominal reference line, an info tooltip, and an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationsEnergyChart` Doughnut breakdown of Operations vs Energy cost (in USD totals). Mirrors the OSS [`OperationsEnergyCostChart`](/reference/ui/components/charts/#operationsenergycostchart). Returns the empty-state placeholder when both totals are zero (the OSS chart hides both slices in that case). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `totals` | `CostSummaryMonetaryTotals \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `OperationsEnergyCostChart` Doughnut breakdown of Operations vs Energy cost (USD per MWh). Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`DoughnutChart`](/reference/ui/components/charts/#doughnutchart); pass `operationalCostsUSD` and `energyCostsUSD` via `data`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | | - | - | | `unit` | `string` | | - | - | | `height` | `number` | | - | - | | `className` | `string` | | - | - | | `isLoading` | `boolean` | | - | - | | `emptyMessage` | `string` | | - | - | | `data` | `Partial<{ energyCostsUSD: number; operationalCostsUSD: number; }>` | | - | - | ### `PowerModeTimelineChart` Timeline chart for power-mode state changes over time. Wraps [`TimelineChart`](/reference/ui/components/charts/#timelinechart) with mining-specific data shaping. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `PowerModeTimelineEntry[]` | | - | Initial power-mode entries (each with start/end ts + mode). | | `dataUpdates` | `PowerModeTimelineEntry[]` | | - | Streaming updates appended to the initial data. | | `isLoading` | `boolean` | | - | Show a loading skeleton instead of the chart. | | `timezone` | `string` | | - | IANA timezone string for x-axis tick formatting. | | `title` | `string` | | - | Chart title. | ### `ProductionCostChart` Production cost over time, overlaid with BTC price. Mirrors the OSS `ProductionCostPriceChart` - both series rendered as bars on the same x-axis (bucket labels derived from `dateRange.period`). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `costLog` | `readonly CostTimeSeriesEntry[]` | ✓ | - | - | | `btcPriceLog` | `readonly BtcPriceTimeSeriesEntry[]` | ✓ | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `RevenueChart` Stacked bar chart displaying monthly revenue per site. Automatically switches between BTC and Sats display based on value scale. Receives pre-fetched data as props — no internal data fetching. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `RevenueDataItem[]` | | - | - | | `isLoading` | `boolean` | | - | - | | `siteList` | `(string \| SiteItem)[]` | | - | - | | `legendPosition` | `Position` | | - | - | | `legendAlign` | `"center" \| "start" \| "end"` | | - | - | ### `ThresholdLineChart` Line chart with optional horizontal threshold lines. Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`LineChart`](/reference/ui/components/charts/#linechart); pass `series` and optional `thresholds` via `data`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | | - | - | | `unit` | `string` | | - | - | | `height` | `number` | | - | - | | `isTall` | `boolean` | | - | When true, uses a taller default height (360px). | | `className` | `string` | | - | - | | `emptyMessage` | `string` | | - | - | | `isLegendVisible` | `boolean` | | - | - | | `data` | `ThresholdLineChartData` | | - | - | | `yTicksFormatter` | `(value: number) => string` | | - | - | ### `TimelineChart` Discrete-event timeline chart (e.g. miner state over time) with a category legend. Supports streaming updates via `newData`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `initialData` | `TimelineChartData` | ✓ | - | - | | `newData` | `TimelineChartData \| undefined` | | - | - | | `skipUpdates` | `boolean \| undefined` | | - | - | | `range` | `ChartRange \| undefined` | | - | - | | `axisTitleText` | `AxisTitleText \| undefined` | | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `title` | `string \| undefined` | | - | - | | `height` | `number \| undefined` | | - | - | # Dashboard Components (/reference/ui/components/dashboard) Components for building dashboard layouts and containers. ## Prerequisites - Complete the [installation](/quickstart/wire-react) - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AlarmsBellButton` Top-bar bell button with a three-line severity badge (critical / high / medium). Counts are caller-provided so the button stays domain-agnostic; pair with `useActiveIncidents` or `useSiteMinerCounts` to wire them. `onClick` fires for the bell itself; pass `onSeverityClick` to make each count its own button (severity-filtered deep-link). The two are independent — clicking a severity does not also fire `onClick`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `counts` | `AlarmsBellButtonCounts \| undefined` | | - | Severity-bucketed alarm counts rendered in the stacked badge. | | `onClick` | `((event: React.MouseEvent) => void) \| undefined` | | - | Click handler — typically opens an alerts panel or routes to /alerts. | | `onSeverityClick` | `((severity: AlarmSeverity, event: React.MouseEvent) => void) \| undefined` | | - | Click handler for an individual severity count. When provided, each badge row becomes its own button so an operator can jump straight to the alerts page filtered by that severity (e.g. `/alerts?severity=critical`). When omitted, the counts render as plain (non-interactive) text. | | `label` | `string \| undefined` | | - | Accessible label. Defaults to "Active alarms". | | `className` | `string \| undefined` | | - | - | ### `DashboardDateRangePicker` Dashboard-friendly wrapper around the core [`DateRangePicker`](/reference/ui/components/forms/#daterangepicker) that speaks `{ start, end }` epoch-millisecond timestamps instead of `Date` objects, so it drops straight into `useDashboardDateRange` from `@tetherto/mdk-react-adapter`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `DashboardDateRange` | ✓ | - | Current range as `{ start, end }` epoch-millisecond timestamps. | | `onChange` | `(next: DashboardDateRange) => void` | ✓ | - | Fires with the next `{ start, end }` window when the user applies a range. | | `dateFormat` | `string \| undefined` | | - | Display format. Defaults to `dd/MM/yyyy`. | | `disabled` | `boolean \| undefined` | | - | Disable the trigger. | | `className` | `string \| undefined` | | - | Optional class hook. | ### `ExportButton` Split-button trigger for downloading the current dashboard state. The left half labels the action (`↓ Export`); the right half opens a dropdown with the available formats and invokes `onExport(format)` on selection. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `onExport` | `(format: ExportFormat) => void` | ✓ | - | Fires with the chosen format when the user picks an item. | | `formats` | `readonly ExportFormat[] \| undefined` | | - | Formats to offer in the dropdown — defaults to `['csv', 'json']`. | | `label` | `string \| undefined` | | - | [**`Button`**](/reference/ui/components/actions/#button) label — defaults to `'Export'`. | | `disabled` | `boolean \| undefined` | | - | Disable the button. | | `className` | `string \| undefined` | | - | Optional class hook on the wrapper. | ### `HeaderConsumptionBox` Single-row consumption cell for the dashboard's header strip. The `1.663` style numeric is rendered in orange (the warning token) to match the Mining OS visual treatment. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | - | | `valueMw` | `number \| undefined` | | - | Current site-level power consumption, in megawatts. | | `unit` | `string \| undefined` | | - | Unit label — defaults to `MW`. | | `className` | `string \| undefined` | | - | - | ### `HeaderEfficiencyBox` Single-row efficiency cell for the dashboard's header strip. Displays the W/TH/s metric derived from `power_w / hashrate_th`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | - | | `valueWthS` | `number \| undefined` | | - | Efficiency in watts per TH/s. | | `unit` | `string \| undefined` | | - | Unit label — defaults to `W/TH/S`. | | `className` | `string \| undefined` | | - | - | ### `HeaderHashrateBox` Two-row hashrate cell for the dashboard's header strip. Shows the MOS-side and pool-side aggregate hashrate side by side. Values fall back to `—` when undefined. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | - | | `mosPhs` | `number \| undefined` | | - | MOS-side aggregate hashrate in PH/s. | | `poolPhs` | `number \| undefined` | | - | Pool-side aggregate hashrate in PH/s. | | `unit` | `string \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) unit label — defaults to `PH/s`. | | `className` | `string \| undefined` | | - | - | ### `HeaderMinersBox` Two-row miner-count cell for the dashboard's header strip. Top row carries the MOS-side `online / error / offline` breakdown; the bottom row shows the pool-side equivalent. Numbers fall back to `—` when undefined. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | Icon shown next to the "Miners" label. Caller-provided so the package stays icon-agnostic. | | `total` | `number \| undefined` | | - | Total miners across the site (denominator of the `158 / 2,188` ratio). | | `online` | `number \| undefined` | | - | Online miners (the `158` numerator). | | `error` | `number \| undefined` | | - | Miners flagged in warning (the small amber count). | | `offline` | `number \| undefined` | | - | Miners offline (the small red count). | | `mosTotal` | `number \| undefined` | | - | Optional MOS-side meta line — total miners reporting to MOS. | | `poolTotal` | `number \| undefined` | | - | Optional pool-side meta — total miners as reported by upstream pools. | | `poolOnline` | `number \| undefined` | | - | Optional pool-side online count (green). | | `poolMismatch` | `number \| undefined` | | - | Optional pool-side mismatch count (red). | | `className` | `string \| undefined` | | - | - | ### `HeaderStatsBar` Horizontal flex strip that hosts the dashboard's stat boxes ([**`HeaderMinersBox`**](/reference/ui/components/dashboard/#headerminersbox), [**`HeaderHashrateBox`**](/reference/ui/components/dashboard/#headerhashratebox), [**`HeaderConsumptionBox`**](/reference/ui/components/dashboard/#headerconsumptionbox), [**`HeaderEfficiencyBox`**](/reference/ui/components/dashboard/#headerefficiencybox)). Lives inside [``](/reference/ui/components/navigation/#appheader) as the middle slot. Slot-based: the caller decides which boxes to render and in which order. The bar interleaves an angled chevron divider between adjacent children to match the Mining OS visual treatment. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `React.ReactNode` | ✓ | - | Stat boxes to render in order, left-to-right. | | `className` | `string \| undefined` | | - | Optional class hook. | ### `ProfileMenu` Top-bar profile dropdown. Wraps the core DropdownMenu primitive with the user-avatar icon as the trigger. Items are caller-provided so the menu surface stays application-driven. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `items` | `ProfileMenuItem[]` | ✓ | - | Items rendered in the dropdown, top-to-bottom. Defaults to a single "Sign out" item. | | `user` | `React.ReactNode` | | - | Optional user label rendered at the top of the dropdown (e.g. an email). | | `icon` | `React.ReactNode` | | - | Override the trigger icon — defaults to the user-avatar icon. | | `label` | `string \| undefined` | | - | Accessible label for the trigger button. | | `className` | `string \| undefined` | | - | - | ### `SiteStatsBar` Site-level summary strip composed from [`WidgetTopRow`](/reference/ui/components/widgets/#widgettoprow) (title + power) and [`GenericDataBox`](/reference/ui/components/widgets/#genericdatabox) (hashrate / miner-count / container-count). Designed to sit at the top of a dashboard page above the chart cards. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | ✓ | - | Site label rendered in the header row. | | `power` | `number \| undefined` | | - | Current site-level power consumption, in watts (or whatever `powerUnit` says). | | `powerUnit` | `string \| undefined` | | - | Display unit for `power` — defaults to `kW`. | | `totalHashrate` | `number \| undefined` | | - | Aggregate hashrate, in TH/s. | | `hashrateUnit` | `string \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) display unit — defaults to `TH/s`. | | `minerCount` | `number \| undefined` | | - | Total miner count across the site. | | `containerCount` | `number \| undefined` | | - | Total container count across the site. | | `isLoading` | `boolean \| undefined` | | - | Render a skeleton bar while data is loading. | | `className` | `string \| undefined` | | - | Optional class hook. | ### `TimelineSelector` Dropdown for picking the dashboard time range. Wraps `core/Select` and the canonical option list from `getTimelineOptions`. Pair with the `useDashboardTimeRange` hook in `@tetherto/mdk-react-adapter`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `string` | ✓ | - | Currently selected timeline value (e.g. `'1m'`, `'5m'`). | | `onChange` | `(next: string) => void` | ✓ | - | Called whenever the user picks a new option. | | `options` | `TimelineOption[] \| undefined` | | - | Available options — defaults to `getTimelineOptions`. Pass a custom list to localise labels or restrict the range. | | `label` | `string \| undefined` | | - | ARIA label / placeholder for the trigger. | | `className` | `string \| undefined` | | - | Tailwind/BEM class hook on the trigger. | # Dashboard Compositions (/reference/ui/components/dashboards) Pre-composed dashboard layouts and templates. ## Prerequisites - Complete the [installation](/quickstart/wire-react) - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActionsSidebar` Full-height side panel for the MiningOS voting/approval workflow. Three sections (only rendered when non-empty): - **Draft** — locally-staged actions not yet sent to the server. - **In review** — actions this user submitted, awaiting votes. - **Requested** — other users' voting actions this user can approve/reject (only shown when the current token has `actions:w`). Open/close state is driven by `actionsStore.sidebarOpen` so the header [`PendingActionsButton`](/reference/ui/components/dashboards/#pendingactionsbutton) and any in-page code can open it without prop-drilling. Mount this once at the app root — it renders nothing when closed. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `className` | `string \| undefined` | | - | Extra class names merged onto the sidebar root element. | ### `Cost` [**`Cost`**](/reference/ui/components/dashboards/#cost) Summary - composite reporting page (single-site). Reads cost-summary view model fields (from [`useCostSummary`](/reference/ui/hooks/utility/#usecostsummary)) and renders: - page header with "[**`Cost`**](/reference/ui/components/dashboards/#cost) Summary" title and an optional action slot (`setCostAction`) pinned to the opposite edge - period selector slot (`controls`) - pass [``](/reference/ui/components/filters/#timeframecontrols) for the OSS-style Year/Month picker or any other date selector element - shared [`CostContent`](/reference/ui/components/dashboards/#costcontent) 2x2 grid (charts + metric tiles) Multi-site is intentionally out of scope for this extraction wave. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `CostSummaryDisplayMetrics \| null` | ✓ | - | - | | `costLog` | `readonly CostTimeSeriesEntry[]` | ✓ | - | - | | `btcPriceLog` | `readonly BtcPriceTimeSeriesEntry[]` | ✓ | - | - | | `totals` | `CostSummaryMonetaryTotals \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `error` | `unknown` | | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `avgAllInCostData` | `readonly AvgAllInCostDataPoint[] \| undefined` | | - | Optional revenue/cost time-series for the Avg All-in [**`Cost`**](/reference/ui/components/dashboards/#cost) panel. | | `controls` | `React.ReactElement>` | ✓ | - | Period selector element. Pass [``](/reference/ui/components/filters/#timeframecontrols) for the OSS-style year/month picker. | | `setCostAction` | `React.ReactElement> \| undefined` | | - | Optional "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" header action slot. A `ReactElement` slot (rather than an href string) so consumers can hand in router-aware components like `` or ` ) } ``` ## Related API - [**`useFormField`**](/reference/ui/hooks/forms/#useformfield) @tetherto/mdk-react-devkit ## Pre-built input field ```tsx ``` ## Related API - [**`FormField`**](/reference/ui/components/forms/#formfield) - [**`useFormField`**](/reference/ui/hooks/forms/#useformfield) ### When to use `FormInput` Use `FormInput` when a standard text-like field should bind directly to `react-hook-form` with the usual label, description, control and validation message. Use the lower-level `FormField` composition when the field needs custom layout or control behaviour. ### `FormInput` example ```tsx ``` @tetherto/mdk-react-devkit ## Date range picker ```tsx ``` Single-date and range-date pickers built on `react-day-picker`. The range picker includes presets and a modal-style popover with Clear / Apply actions. ## `DatePicker` props | Prop | Type | Required | Default | Description | | ------------------- | ------------------------------- | -------- | -------------- | --------------------------------- | | `selected` | `Date` | no | — | Selected date. | | `onSelect` | `(date?: Date) => void` | no | — | Setter. | | `placeholder` | `string` | no | `"Pick a date"`| Trigger button placeholder. | | `dateFormat` | `string` | no | `"MM/dd/yyyy"` | `date-fns` format string. | | `disabled` | `boolean` | no | `false` | Disable the trigger. | | `triggerClassName` | `string` | no | — | Class names on the trigger button.| | `calendarClassName` | `string` | no | — | Class names on the day-picker. | ## `DateRangePicker` props Adds `showPresets`, `presets: PresetItem[]`, `allowFutureDates`, and `modalClassName`. `selected` / `onSelect` use `DateRange`. ## Data contracts ```ts type DateRange = { from: Date | undefined; to?: Date | undefined }; type PresetItem = { label: string; value: DateRange }; ``` ## Example ```tsx /** * Runnable example for DatePicker and DateRangePicker. */ const [date, setDate] = useState() const [range, setRange] = useState() return (
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `Cascader` Two-panel hierarchical selector for picking a leaf value from a nested tree (categories → subcategories → leaf). Features: - Two-column layout: categories on left, options on right - Single or multiple selection modes - Search/filter functionality via [**`TagInput`**](/reference/ui/components/forms/#taginput) - Category-level selection (select/deselect all children) - Indeterminate state for partial selections - [**`Tag`**](/reference/ui/components/display/#tag) display for multiple selections - Keyboard navigation support - Disabled state support `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `options` | `CascaderOption[]` | ✓ | - | Hierarchical options to display in the cascader Parent options with children appear in the left panel Child options appear in the right panel when parent is selected | | `value` | `CascaderValue \| CascaderValue[] \| undefined` | | - | Current selected value(s) - For single select: CascaderValue (e.g., ['category', 'option']) - For multiple select: CascaderValue[] (e.g., [['cat1', 'opt1'], ['cat2', 'opt2']]) | | `onChange` | `((value: CascaderValue \| CascaderValue[] \| null) => void) \| undefined` | | - | Callback when selection changes - For single select: receives CascaderValue or null - For multiple select: receives CascaderValue[] or null | | `multiple` | `boolean \| undefined` | | `false` | Enable multiple selection mode - true: Shows checkboxes, allows multiple selections, displays selected items as tags - false: Shows radio buttons, allows single selection | | `placeholder` | `string \| undefined` | | `'Select...'` | Placeholder text shown in the input when no selections are made | | `disabled` | `boolean \| undefined` | | `false` | Disable the entire cascader (input and all options) | | `className` | `string \| undefined` | | - | Custom className for the root cascader element | | `dropdownClassName` | `string \| undefined` | | - | Custom className for the dropdown panels container | ### `Checkbox` [**`Checkbox`**](/reference/ui/components/forms/#checkbox) component with full customization `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `size` | `CheckboxSize \| undefined` | | `'md'` | Size variant of the checkbox | | `color` | `"success" \| "warning" \| "error" \| "primary" \| "default" \| undefined` | | `'primary'` | Color variant when checked | | `radius` | `BorderRadius \| undefined` | | `'none'` | Border radius variant | | `className` | `string \| undefined` | | - | Custom className for the root element | | `indicatorClassName` | `string \| undefined` | | - | Custom className for the indicator element | | `onCheckedChange` | `(((checked: CheckedState) => void) & ((checked: CheckedState) => void)) \| undefined` | | - | Callback when the checked state changes | ### `CurrencyToggler` [**`CurrencyToggler`**](/reference/ui/components/forms/#currencytoggler) component for switching between currencies `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `string` | ✓ | - | - | | `className` | `string \| undefined` | | - | - | | `currencies` | `(string \| CurrencyItem)[]` | ✓ | - | - | | `onChange` | `(currency: string) => void` | ✓ | - | - | ### `DatePicker` Single-date selection component built on react-day-picker with the MDK dark theme. Controlled via `selected` / `onSelect`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `selected` | `Date \| undefined` | | - | Currently selected date | | `onSelect` | `((date: Date \| undefined) => void) \| undefined` | | - | Callback when date changes | | `placeholder` | `string \| undefined` | | `"Pick a date"` | Placeholder text when no date is selected | | `dateFormat` | `string \| undefined` | | `"MM/dd/yyyy"` | Date format for display | | `disabled` | `(boolean & (Matcher \| Matcher[])) \| undefined` | | `false` | Whether the picker is disabled | | `triggerClassName` | `string \| undefined` | | - | Custom className for the trigger button | | `calendarClassName` | `string \| undefined` | | - | Custom className for the calendar | ### `DateRangePicker` Date-range selection component with preset shortcuts (last 7/14/30/90 days) and a modal interface. Controlled via `selected` / `onSelect`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `selected` | `DateRange \| undefined` | | - | Selected date range | | `onSelect` | `((range: DateRange \| undefined) => void) \| undefined` | | - | Callback when date range changes | | `placeholder` | `string \| undefined` | | `"Pick a date range"` | Placeholder text when no range is selected | | `dateFormat` | `string \| undefined` | | `"MM/dd/yyyy"` | Date format for display | | `disabled` | `(boolean & (Matcher \| Matcher[])) \| undefined` | | `false` | Whether the picker is disabled | | `showPresets` | `boolean \| undefined` | | `true` | Whether to show preset buttons | | `presets` | `PresetItem[] \| undefined` | | - | Custom preset items | | `allowFutureDates` | `boolean \| undefined` | | `false` | Whether to allow future dates | | `triggerClassName` | `string \| undefined` | | - | Custom className for the trigger button | | `calendarClassName` | `string \| undefined` | | - | Custom className for the calendar | | `modalClassName` | `string \| undefined` | | - | Custom className for the modal | ### `Form` React Hook [**`Form`**](/reference/ui/components/forms/#form) provider wrapper. Pass the result of `useForm()` as `form` and render fields via [`FormField`](/reference/ui/components/forms/#formfield) / [`FormItem`](/reference/ui/components/forms/#formitem) / [`FormLabel`](/reference/ui/components/forms/#formlabel) / [`FormControl`](/reference/ui/components/forms/#formcontrol) / [`FormMessage`](/reference/ui/components/forms/#formmessage). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `form` | `UseFormReturn` | ✓ | - | - | | `children` | `React.ReactNode` | ✓ | - | - | ### `FormCascader` Pre-built [**`Cascader`**](/reference/ui/components/forms/#cascader) field component with integrated form state. Perfect for hierarchical selections like categories and subcategories. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `options` | `CascaderOption[]` | ✓ | - | - | | `multiple` | `boolean \| undefined` | | - | - | | `cascaderProps` | `Omit, "onChange" \| "value" \| "options" \| "placeholder"> \| undefined` | | - | - | ### `FormCheckbox` Pre-built [**`Checkbox`**](/reference/ui/components/forms/#checkbox) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `checkboxProps` | `({ size?: CheckboxSize \| undefined; color?: ComponentColor \| undefined; radius?: BorderRadius \| undefined; className?: string \| undefined; indicatorClassName?: string \| undefined; onCheckedChange?: ((checked: CheckedState) => void) \| undefi… /* see source */` | | - | - | | `layout` | `"row" \| "column" \| undefined` | | - | - | ### `FormControl` Slot-based wrapper that injects ARIA attributes onto its child input element without adding an extra DOM wrapper. `agent-ready` ### `FormDatePicker` Pre-built [**`DatePicker`**](/reference/ui/components/forms/#datepicker) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `datePickerProps` | `Omit<{ selected?: Date \| undefined; onSelect?: ((date: Date \| undefined) => void) \| undefined; placeholder?: string \| undefined; dateFormat?: string \| undefined; disabled?: boolean \| undefined; triggerClassName?: string \| undefined; calenda… /* see source */` | | - | - | ### `FormDescription` Optional helper text displayed below the input. `agent-ready` ### `FormField` Wraps react-hook-form's Controller and provides field context to descendants. `agent-ready` ### `FormInput` Pre-built [**`Input`**](/reference/ui/components/forms/#input) field component with integrated form state. Reduces boilerplate by combining [**`FormField`**](/reference/ui/components/forms/#formfield), [**`FormItem`**](/reference/ui/components/forms/#formitem), [**`FormLabel`**](/reference/ui/components/forms/#formlabel), [**`FormControl`**](/reference/ui/components/forms/#formcontrol), and [**`FormMessage`**](/reference/ui/components/forms/#formmessage). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `type` | `React.HTMLInputTypeAttribute \| undefined` | | - | - | | `variant` | `"search" \| "default" \| undefined` | | - | - | | `inputProps` | `Omit & React.RefAttributes, "type" \| "variant"> \| undefined` | | - | - | ### `FormItem` Layout wrapper for a form field. Generates a unique ID for accessibility linking. `agent-ready` ### `FormLabel` [**`Label`**](/reference/ui/components/forms/#label) that auto-links to the form field input via generated IDs. Applies error styling when the field has a validation error. `agent-ready` ### `FormMessage` Displays the validation error message from react-hook-form field state. Falls back to children if no error is present. Always renders to prevent layout shift when errors appear. `agent-ready` ### `FormRadioGroup` Pre-built [**`RadioGroup`**](/reference/ui/components/forms/#radiogroup) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `options` | `FormRadioOption[]` | ✓ | - | - | | `orientation` | `"horizontal" \| "vertical" \| undefined` | | - | - | | `radioGroupProps` | `Omit<{ orientation?: "horizontal" \| "vertical" \| undefined; noGap?: boolean \| undefined; className?: string \| undefined; } & Omit, "ref"> & React.RefAttributes, "defaultV… /* see source */` | | - | - | ### `FormSelect` Pre-built [**`Select`**](/reference/ui/components/forms/#select) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `options` | `FormSelectOption[]` | ✓ | - | - | | `selectProps` | `Omit \| undefined` | | - | - | ### `FormSwitch` Pre-built [**`Switch`**](/reference/ui/components/forms/#switch) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `switchProps` | `Omit<{ size?: ComponentSize \| undefined; color?: ComponentColor \| undefined; radius?: BorderRadius \| undefined; className?: string \| undefined; thumbClassName?: string \| undefined; } & Omit, "label" \| "value" \| "placeholder" \| "onTagsChange"> \| undefined` | | - | - | ### `FormTextArea` Pre-built [**`TextArea`**](/reference/ui/components/forms/#textarea) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `textAreaProps` | `(Omit & React.RefAttributes) \| undefined` | | - | - | ### `Input` Text input with optional label, prefix/suffix slots, and a `search` variant. Forwards refs and all native `` attributes. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `error` | `string \| undefined` | | - | Validation error message. When provided, displays error styling (red border) and the message below the input. | | `label` | `string \| undefined` | | - | Optional label displayed above the input | | `prefix` | `React.ReactNode` | | - | Prefix element displayed before the input (left side) | | `variant` | `"search" \| "default" \| undefined` | | `'default'` | Variant of the input - `default`: Standard text input - `search`: [**`Input`**](/reference/ui/components/forms/#input) with magnifying glass icon on the right | | `size` | `InputSize \| undefined` | | `'default'` | Size of the input - `default`: padding 10px 12px, icon 16px - `medium`: padding 6px 12px, icon 12px | | `wrapperClassName` | `string \| undefined` | | - | Custom className for the root wrapper | | `suffix` | `React.ReactNode` | | - | Suffix element displayed after the input (right side) | ### `Label` Accessible text label for form controls. Associates with an input via `htmlFor` and supports a required-mark indicator. Built on Radix [**`Label`**](/reference/ui/components/forms/#label). `agent-ready` ### `MultiLevelSelect` Multi-level select component with collapsible sections. `agent-ready` ### `MultiSelect` Multi-select picker built on Radix [**`Popover`**](/reference/ui/components/overlays/#popover) + [**`Checkbox`**](/reference/ui/components/forms/#checkbox). Sibling to [` )} /> ) } ``` @tetherto/mdk-react-devkit ## Reset form state ```tsx ``` ## Related API - [**`Form`**](/reference/ui/components/forms/#form) - [**`FormInput`**](/reference/ui/components/forms/#forminput) ### When to use `useFormReset` Use `useFormReset` when resetting a [`react-hook-form`](https://react-hook-form.com/) form also needs before/after callbacks or a reusable dirty-state guard. Call the form object's `reset` method directly when no lifecycle behaviour is needed. ### `useFormReset` workflow Create the form with `useForm`, pass that instance to `useFormReset`, then wire the returned `resetForm` handler to a reset or cancel action. The hook reports `isDirty` from the same form state so the action can be disabled until values change. ### `useFormReset` prerequisite The hook requires a configured `react-hook-form` instance. It composes with [`Form`](/reference/ui/components/forms/#form) and fields such as [`FormInput`](/reference/ui/components/forms/#forminput); it does not create or submit the form. ### `useFormReset` example ```tsx type MinerFields = { name: string; ip: string } function MinerEditForm({ onSubmit }: { onSubmit: (values: MinerFields) => void }) { const form = useForm({ defaultValues: { name: '', ip: '' } }) const { resetForm, isDirty } = useFormReset({ form, onAfterReset: () => console.log('Form reset'), }) return (
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useFormField` Read-only context hook for form field children — returns the field's id, error state, and ARIA attributes. ```typescript () => UseFormFieldReturn ``` ### `useFormReset` Hook to handle form reset with callbacks. ```typescript ({ form, onBeforeReset, onAfterReset, }: UseFormResetOptions) => UseFormResetReturn ``` # Miscellaneous Hooks (/reference/ui/hooks/misc) Miscellaneous and general-purpose hooks. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useEnergyReportSite` Merges site energy consumption (v2 /auth/metrics/consumption) with snapshot tail-log and container list data for the Energy report site tab. ```typescript ({ dateRange: _dateRange, consumptionLog, consumptionLoading, consumptionFetching, consumptionError, nominalPowerAvailabilityMw, nominalConfigLoading, tailLog, tailLogLoading, containers, containersLoading, }: UseEnergyReportSiteInput) => U… /* see source */ ``` ### `useHashBalance` Derives hash-balance metrics and chart datasets from finance log entries for the active date range, currency, and timeframe type. Used by hash balance panels. ```typescript ({ data, log, currency, dateRange, timeframeType, }: UseHashBalanceInput) => { siteHashRevenueChartData: BarChartDataResult; networkHashpriceChartData: BarChartDataResult; combinedCostChartData: BarChartDataResult; isEmpty: boolean; periodT… /* see source */ ``` ### `useSubsidyFees` Aggregates raw subsidy-fee log entries into chart-ready datasets keyed by the active period type (day / week / month / year) and surfaces a summary for the matching reporting widgets. Used by [`SubsidyFee`](/reference/ui/components/dashboards/#subsidyfee); expose to downstream apps that need to recompose the same datasets in a custom UI. ```typescript ({ data, log, dateRange }: UseSubsidyFeesInput) => { summary: SubsidyFeeSummary; filteredLog: SubsidyFeesLogEntry[]; aggregatedData: AggregatedPeriodData[]; subsidyFeesChartData: BarChartDataResult; averageFeesChartData: BarChartDataResult;… /* see source */ ``` ### `useUpdateExistedActions` Mutation hook that updates only the changed fields of an existing action record. Removes tags from existing pending submissions if those tags belong to the newly selected devices; if the resulting tag list is empty, the submission itself is removed. This avoids duplicate pending actions for the same device. ```typescript () => { updateExistedActions: ({ actionType, pendingSubmissions, selectedDevices, }: UpdateExistedActionsParams) => void; } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useHashrate` Base hook for a single [**`Hashrate`**](/reference/ui/components/charts/#hashrate) tab (single-site mode). Normalizes a grouped-hashrate query result to the `{ log, isLoading, error }` shape consumed by [``](/reference/ui/components/charts/#hashratesiteview), [``](/reference/ui/components/charts/#hashrateminertypeview), and [``](/reference/ui/components/charts/#hashrateminingunitview). Call once per tab the consumer needs to render - each tab fetches independently because they use different `groupBy` axes and (typically) different date ranges. ```typescript ({ query }?: UseHashrateOptions) => UseHashrateResult ``` ### `useOperationsDashboard` Shapes raw operational metric logs into chart-ready payloads for the four operational-dashboard cards. DI-style: it never fetches - wire your data layer (RTK Query, TanStack, fixtures) and pass the results in. All unit conversion and series shaping happens here so the chart components stay purely presentational. ```typescript (input?: Partial<{ hashrate: Partial<{ log: readonly OperationsTrendPoint[]; nominalValue: number | null; isLoading: boolean; error: unknown; }>; consumption: Partial<{ log: readonly OperationsTrendPoint[]; nominalValue: number | null; isLo… /* see source */ ``` # Navigation Hooks (/reference/ui/hooks/navigation) Hooks for navigation and routing state. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ## Persist sidebar state ```tsx ``` ## Related API - [**`useSidebarSectionState`**](/reference/ui/hooks/navigation/#usesidebarsectionstate) - [**`useSidebarExpandedState`**](/reference/ui/hooks/navigation/#usesidebarexpandedstate) ### When to use sidebar state hooks Use `useSidebarExpandedState` for the whole sidebar's expanded state and `useSidebarSectionState` for an individual section. Both persist their value in `localStorage`, so navigation choices survive a reload. ### Sidebar state example ```tsx useSidebarExpandedState, useSidebarSectionState, } from '@tetherto/mdk-react-devkit' function AppSidebar() { const [expanded, setExpanded] = useSidebarExpandedState(false) const [devicesOpen, setDevicesOpen] = useSidebarSectionState('devices', true) return ( ) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useSidebarExpandedState` Custom hook to persist sidebar expanded state in localStorage ```typescript (defaultExpanded: boolean) => [boolean, (expanded: boolean) => void] ``` ### `useSidebarSectionState` Custom hook to persist individual section open/closed states in localStorage ```typescript (sectionId: string, defaultOpen: boolean) => [boolean, (open: boolean) => void] ``` # Operations Centre Hooks (/reference/ui/hooks/op-centre) Hooks for operations centre functionality and monitoring. ## From `@tetherto/mdk-react-adapter` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useCabinetDevices` Fetches one LV cabinet's family of devices — the powermeters and temperature sensors whose `info.pos` sits under the cabinet `root` (`buildCabinetDetailParams`) — polled at the Op-Centre realtime cadence and flattened across the per-… ```typescript (root: string, options: UseCabinetDevicesOptions = {}) => UseCabinetDevicesResult ``` ### `useCabinetGroups` Fetches the Explorer cabinet-tab devices (powermeters + temperature sensors) and groups them by their owning container (`info.container`); devices without a container assignment (site-level meters) collect under the `site` group, sorted la… ```typescript (options: UseCabinetGroupsOptions = {}) => UseCabinetGroupsResult ``` ### `useContainerSettings` Fetches per-model container thresholds/parameters from `GET /auth/global/data?type=containerSettings`. Feeds the threshold status indicators (tank pressure, oil/water temperature) on the container widgets and detail tabs. ```typescript (options: UseContainerSettingsOptions = {}) => UseContainerSettingsResult ``` ### `useContainerSnapshots` Fetches the detail snapshots for the selected containers — the richer projection (`buildContainerDetailParams`) that carries `container_specific.pdu_data` plus the tank / cooling / power-mode config the detail panel controls read. `c… ```typescript (containerKeys: string[], options: UseContainerSnapshotsOptions = {}) => UseContainerSnapshotsResult ``` ### `useContainerWidgets` Data source for the Site Overview Container Widgets grid: the container inventory (one card per container) plus the latest per-miner realtime aggregate sample the cards derive their summaries from. Card-shaped payload derivation lives with… ```typescript (options: UseContainerWidgetsOptions = {}) => UseContainerWidgetsResult ``` ### `useExplorerList` Fetches the thing list behind one Explorer tab (`container` / `miner` / `cabinet`) from `GET /auth/list-things`, tag-filtered and projected by the foundation's Explorer params builder. Rows are flattened across the per-Kernel envelope so r… ```typescript (tab: ExplorerTabValue, options: UseExplorerListOptions = {}) => UseExplorerListResult ``` ### `useFeatureFlags` Fetches the deployment feature flags from `GET /auth/featureConfig` (camelCase route — there is no `/auth/feature-config`). Static deployment config — fetched once per session, no polling. Gates optional tabs/sub-routes (`containerCharts`,… ```typescript (options: UseFeatureFlagsOptions = {}) => UseFeatureFlagsResult ``` ### `usePduLayout` Fetches a container type's static PDU socket grid from `GET /auth/pdu-layout`. The grid is provisioned in the container worker's `pduGridLayout` config keyed by the exact type string — an unprovisioned type is a backend 400 (`ERR_PDU_LAYOU… ```typescript (params: UsePduLayoutParams, options: UsePduLayoutOptions = {}) => UsePduLayoutResult ``` ### `useRackLayout` Fetches the rack structure for a worker type from `GET /auth/list-racks` (`type` is required — the backend 400s with `ERR_TYPE_INVALID` without it). Feeds the Explorer rack grouping. ```typescript (params: UseRackLayoutParams, options: UseRackLayoutOptions = {}) => UseRackLayoutResult ``` ### `useSite` Fetches the configured site label from `GET /auth/site`. Static deployment config — fetched once per session, no polling. ```typescript (options: UseSiteOptions = {}) => UseSiteResult ``` ### `useThingComment` Device-comment writes for the Explorer detail panel — add, edit, and delete against `/auth/thing/comment` (the author is stamped server-side from the session token). Comments ride on the thing rows themselves (`comments` in the list-things… ```typescript () => UseThingCommentResult ``` ### `useThingDetail` Fetches a single thing by id from `GET /auth/list-things` with the full Op Centre field projection — the data source for the Explorer detail panel and the container Thing-detail view. ```typescript (id: string | undefined, options: UseThingDetailOptions = {}) => UseThingDetailResult ``` ## From `@tetherto/mdk-react-devkit` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useExplorerSelection` Bridges the Explorer table selection into the shared `devicesStore` that the write-control cards read. Given the active tab and the table's row-selection, it dispatches the matching setters — containers → `selectMultipleContainers`, miners → `setSelectDevice` + `selectDeviceTag`, cabinets → `selectLVCabinet` — and, for containers/miners, fetches the richer detail snapshots (`useContainerSnapshots`) so the controls see the full `last.snap` config (tank / cooling / power-mode) the lean list projection omits, then derives the per-socket selection into the store. Selections are reset whenever the selection or tab changes and on unmount, so a stale selection can never drive the panel. ```typescript ({ deviceType, rows, selected, }: UseExplorerSelectionParams) => UseExplorerSelectionResult ``` ### `useMinerDetail` Reads the miner selection the [`useExplorerSelection`](/reference/ui/hooks/op-centre/#useexplorerselection) bridge writes into `devicesStore` and shapes the head miner for the read-only cards of the miner detail panel — the info rows ([`MinerInfoCard`](/reference/ui/components/widgets/#minerinfocard)) and the per-chip frequency / temperature stats ([`MinerChipsCard`](/reference/ui/components/widgets/#minerchipscard)). The write controls ([`MinerControlsCard`](/reference/ui/components/widgets/#minercontrolscard)) and the aggregate stats ([`StatsGroupCard`](/reference/ui/components/widgets/#statsgroupcard)) read the same store directly. ```typescript () => UseMinerDetailResult ``` # Permission Hooks (/reference/ui/hooks/permission) Hooks for checking permissions and managing access control. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter ## Check a permission ```tsx ``` ## Related API - [**`useAuth`**](/reference/ui/hooks/store/#useauth) ### When to use `useCheckPerm` Use `useCheckPerm` for a single reactive permission, write-access or capability gate in a React component. It reads the current permissions from the same auth store exposed by [`useAuth`](/reference/ui/hooks/store/#useauth). ### `useCheckPerm` example ```tsx function EditUsersButton() { const canEditUsers = useCheckPerm({ perm: 'users:write' }) return canEditUsers ? : null } ``` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useCheckPerm` Hook to check if the current user has a specific permission. Reads `permissions` from the headless `authStore` via `@tetherto/mdk-react-adapter`. ```typescript ({ perm, write, cap }: PermissionCheck) => boolean ``` ### `useHasPerms` Hook returning a permission-check callback bound to the current `authStore.permissions`. ```typescript () => ((req: PermissionRequest) => boolean) ``` ### `useIsFeatureEditingEnabled` Hook to check if the current user has the capability to edit feature flags. ```typescript () => boolean ``` # Settings Hooks (/reference/ui/hooks/settings) Hooks for managing user settings and preferences. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ## Header controls ```tsx ``` ## Related API - [**`useNotification`**](/reference/ui/hooks/feedback/#usenotification) ### When to use `useHeaderControls` Use `useHeaderControls` in settings UI that reads or changes the shared header item preferences. It exposes the current preferences together with focused toggle and reset handlers. ### `useHeaderControls` notification behaviour `handleToggle` and `handleReset` show a success notification. Avoid calling either handler from rapidly changing state or an effect that can repeat, because each invocation creates another toast. ### `useHeaderControls` example ```tsx function HeaderSettings() { const { preferences, handleToggle, handleReset } = useHeaderControls() return (
{Object.entries(preferences).map(([key, visible]) => ( ))}
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useHeaderControls` Read/write hook for the global header-controls store (toggles, sticky flag, theme). ```typescript () => { preferences: HeaderPreferences; isLoading: boolean; error: null; handleToggle: (key: keyof HeaderPreferences, value: boolean) => void; handleReset: () => void; } ``` # Store Hooks (/reference/ui/hooks/store) Hooks for accessing Zustand stores and managing application state. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter ## Core store hooks ```tsx ``` ## Related API - [**`useNotification`**](/reference/ui/hooks/feedback/#usenotification) ### When to use core store hooks Use these hooks when a React component needs a reactive view of the headless Foundation stores: `useAuth` for session state, `useTimezone` for the selected IANA timezone, `useNotifications` for the unread count and `useActions` for the pending operator-action queue. Non-React code can read the corresponding Foundation store directly. ### Core store hooks example ```tsx useActions, useAuth, useNotifications, useTimezone, } from '@tetherto/mdk-react-adapter' function ShellStatus() { const { token } = useAuth() const { timezone } = useTimezone() const { count } = useNotifications() const { pendingSubmissions } = useActions() return (
Session
{token ? 'Active' : 'Signed out'}
Timezone
{timezone}
Unread
{count}
Pending actions
{pendingSubmissions.length}
) } ``` @tetherto/mdk-react-adapter ## Device store access ```tsx ``` ### When to use `useDevices` Use `useDevices` in React components that need the headless device inventory or current device selection. It is the React-bound view of `devicesStore`; code outside React should use the foundation store directly. ### `useDevices` example ```tsx function DeviceToolbar() { const { selectedDevices, setSelectedDevices } = useDevices() return (

Selected: {selectedDevices.length}

) } ``` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useActions` React-bound view of the headless `actionsStore` (command queue + lifecycle). ```typescript () => ActionsStore ``` ### `useAuth` React-bound view of the headless `authStore`. Equivalent of `useStore(authStore)` — exposed as a hook for ergonomic callsites. ```typescript () => AuthStore ``` ### `useDevices` React-bound view of the headless `devicesStore` (miners, containers, PDUs). ```typescript () => DevicesStore ``` ### `useNotifications` React-bound view of the headless `notificationStore` (toast queue + history). ```typescript () => NotificationStore ``` ### `useTimezone` React-bound view of the headless `timezoneStore` (selected IANA zone). ```typescript () => TimezoneStore ``` # Table Hooks (/reference/ui/hooks/tables) Hooks for table components and data management. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ## Explorer data ```tsx ``` ## Related API - [**`DataTable`**](/reference/ui/components/tables/#datatable) @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useExplorerData` Explorer list data hook: fetches the things behind one tab (`useExplorerList`) and shapes them for [``](/reference/ui/components/tables/#deviceexplorer) — applying the toolbar's search + filter selections client-side and deriving the search-autocomplete and filter-cascader options from the fetched rows. The tag-based backend query lives in `@tetherto/mdk-ui-foundation`; this hook only reads snapshot fields for display filtering. Search, status-filter and (in [`DeviceExplorer`](/reference/ui/components/tables/#deviceexplorer)) column sort are all **client-side**, over a tag-filtered, capped fetch — this mirrors MOS/the reference app. Fine for containers/cabinets; for very large miner fleets this fetches the cap and filters in the browser (no server paging). Push status into the foundation query + wire `limit`/`offset` if that ceiling is ever hit. ```typescript (options: UseExplorerDataOptions) => UseExplorerDataResult ``` # UI Hooks (/reference/ui/hooks/ui) General-purpose UI state hooks. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useMinerDuplicateValidation` Async validation hook that flags duplicate miners against the device inventory (MAC / serial / IP / human-facing code). ```typescript () => { duplicateError: boolean; isDuplicateCheckLoading: boolean; checkDuplicate: (selectedEditSocket: { miner?: { id?: string | undefined; } | undefined; } | null, { macAddress, serialNumber, addre… ``` ### `usePduViewer` Pan/zoom controller for the PDU floor-plan viewer. ```typescript () => UsePduViewerReturn ``` ### `useStaticMinerIpAssignment` Derives the static IP address a miner should receive based on its physical position (container number + socket coordinates). ```typescript (selectedEditSocket: Partial) => { minerIp: string; setMinerIp: React.Dispatch>; isStaticIpAssignment: boolean; } ``` # Utility Hooks (/reference/ui/hooks/utility) General-purpose utility hooks. ## From `@tetherto/mdk-react-adapter` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useBeepSound` Plays a repeating beep sound at a configurable interval. ```typescript ({ isAllowed = DEFAULTS.IS_ALLOWED, volume = DEFAULTS.VOLUME, delayMs = DEFAULTS.DELAY_MS, }: UseBeepSoundOptions = {}) => void ``` ### `useContextualModal` Headless open/close state for a modal that needs to remember the subject it was opened against (the row being edited, the device being inspected, etc.). ```typescript ({ onOpen, onClose, }: UseContextualModalParams = {}) => { modalOpen: boolean; handleClose: () => void; handleOpen: (sub: T | null) => void; subject: T | null; setSubject: React.Dispatch DeviceResolution ``` ### `useKeyDown` Tracks whether a specific keyboard key is currently held down. ```typescript (keyName: string) => boolean ``` ### `useLocalStorage` Custom hook for type-safe localStorage access with cross-tab sync. ```typescript (key: string, defaultValue: T) => [T, (value: T | ((prev: T) => T)) => void, VoidFunction] ``` ### `usePagination` Custom hook for managing pagination state ```typescript (args: PaginationArgs = {}) => UsePaginationReturn ``` ### `usePlatform` SSR-safe React hook returning the detected client OS. ```typescript () => PlatformResult ``` ### `useSubtractedTime` Returns `Date.now() - diff`, refreshing on a fixed interval (default 5s). ```typescript (diff: number, interval = DEFAULT_INTERVAL) => number ``` ### `useTimezoneFormatter` Timezone-aware date formatting hook. ```typescript () => UseTimezoneFormatterReturn ``` ### `useWindowSize` Hook to track window size changes ```typescript () => WindowSize ``` ## From `@tetherto/mdk-react-devkit` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useCostSummary` Base hook for the cost-summary reporting page (single-site mode). Owns the date-range / period UI state and the pure transform from a v2 `/auth/finance/cost-summary` response into headline metrics and time-series. Consumers wire their own fetch (RTK Query, TanStack Query, fixtures, ...) and pass the result through `query` - the hook never fetches itself. Multi-site mode is composed at the page level (T-13) by feeding a different response shape to the same view-model primitives; this base hook stays single-site to keep the input contract narrow. ```typescript ({ query, ...dateRangeOptions }?: UseCostSummaryOptions) => { queryParams: CostSummaryQueryParams | null; isLoading: boolean; error: {} | null; metrics: CostSummaryDisplayMetrics | null; costLog: readonly CostTimeSeriesEntry[]; btcPriceLog:… /* see source */ ``` # Widget Hooks (/reference/ui/hooks/widgets) Hooks for dashboard widgets and data cards. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useContainerThresholds` Hook that reads and updates the temperature/pressure/power thresholds for a single container. Features: - Load and save container threshold settings - Auto-save default thresholds when none exist - Validate and auto-adjust overlapping thresholds - Handle reset to saved/default values ```typescript ({ data, onSave, }: UseContainerThresholdsProps) => UseContainerThresholdsReturn ``` ### `useFinancialDateRange` Resolves the active financial date range (start/end) used by every reporting-section query. ```typescript (options?: UseFinancialDateRangeOptions | undefined) => UseFinancialDateRangeResult ``` # Types (/reference/ui/types) TypeScript type definitions exported from `@tetherto/mdk-react-devkit` and `@tetherto/mdk-ui-foundation`. ## Browse by category ### From `@tetherto/mdk-react-devkit` | Category | Description | |----------|-------------| | [Cards](/reference/ui/types/cards) | Card component type definitions | | [General](/reference/ui/types/devkit-general) | General component and hook types | ### From `@tetherto/mdk-ui-foundation` | Category | Description | |----------|-------------| | [API](/reference/ui/types/api) | API request/response types | | [Dashboard](/reference/ui/types/foundation-dashboard) | Dashboard data types | | [General](/reference/ui/types/foundation-general) | General utility types | ## Import pattern ```tsx ``` # API Types (/reference/ui/types/api) Type definitions for API requests and responses. ## Package `@tetherto/mdk-ui-foundation` ## Types @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ActionsParams` Free-form query parameters for `GET /auth/actions`. Array values are serialized comma-separated (e.g. `?status=VOTING,APPROVED`). ```typescript type ActionsParams = Record ``` ### `ActionTypeQuery` One entry in the `queries` array sent to `GET /auth/actions?queries=…`. ```typescript type ActionTypeQuery = { type: 'voting' | 'ready' | 'executing' | 'done' | string opts?: { reverse?: boolean; limit?: number; [key: string]: unknown } } ``` ### `AggregatedPool` Aggregated pool row from `GET /auth/pools` (hashrate / workers / balance / revenue / summary). Feeds the Dashboard pool panel, not the Pools list. ```typescript type AggregatedPool = { name?: string hashrate?: number workers?: number balance?: number revenue?: number [key: string]: unknown } ``` ### `AlertSeverity` Severity literal expected by the `ActiveIncidentsCard` row component. ```typescript type AlertSeverity = 'critical' | 'high' | 'medium' ``` ### `AuthTokenRequest` Request body for `POST /auth/token`. The token endpoint refreshes the session and (optionally) downgrades the role set. ```typescript type AuthTokenRequest = { roles?: string[] ttl?: number ips?: string[] scope?: string } ``` ### `AuthTokenResponse` Response from `POST /auth/token`. ```typescript type AuthTokenResponse = { token: string } ``` ### `CancelActionsPayload` Arguments for `DELETE /auth/actions/:type/cancel?ids=<comma>`. ```typescript type CancelActionsPayload = { /** Action type URL segment (e.g. `voting`). */ type: string ids: Array } ``` ### `ContainerPoolStat` Per-container override-count row from `GET /auth/pools/stats/containers`. ```typescript type ContainerPoolStat = { container: string overriddenConfig?: number [key: string]: unknown } ``` ### `ContainerSettingsEntry` One row of `GET /auth/global/data?type=containerSettings` (verified live 2026-07-01 — the response is a flat array, not the per-Kernel envelope). `thresholds` is keyed by threshold type (`oilTemperature`, `tankPressure`, `waterTemperature`… ```typescript type ContainerSettingsEntry = { model: string site?: string parameters?: Record thresholds?: Record } ``` ### `ContainerThresholdLevels` Threshold band for one container parameter. Which levels are present varies per parameter (verified live: `oilTemperature` carries `alert`/`alarm`, `waterTemperature` carries `alarmLow`/`alarmHigh`). ```typescript type ContainerThresholdLevels = { criticalLow?: number alarmLow?: number alert?: number normal?: number alarm?: number alarmHigh?: number criticalHigh?: number } ``` ### `DeviceAlert` Single alert record carried on a device under `last.alerts`. The list-things endpoint returns alerts as nested arrays — see `getAlertsForDevices` for the canonical extractor. ```typescript type DeviceAlert = { uuid?: string name: string description: string message?: string severity: string createdAt: number | string type?: string } ``` ### `ExtDataParams` Query parameters for `GET /auth/ext-data` — a small key-value gateway the backend exposes for non-tail-log data sources (minerpool, mempool, etc.). ```typescript type ExtDataParams = { /** Provider id — e.g. `minerpool`, `mempool`. */ type: string /** JSON-stringified provider-specific filter. */ query?: string } ``` ### `FeatureConfigResponse` Response from `GET /auth/featureConfig` (note the camelCase path — there is no `/auth/feature-config` route). Typed loosely: the flag set is deployment-specific. The backend may also return multi-site keys (`isMultiSiteModeEnabled`, `siteL… ```typescript type FeatureConfigResponse = { [key: string]: unknown } ``` ### `GlobalDataParams` Query parameters for `GET /auth/global/data`. `type` selects the global data set (e.g. `containerSettings`); `model` optionally narrows container settings to one settings-model (`bd`, `mbt`, `hydro`, `immersion`). ```typescript type GlobalDataParams = { type: string model?: string overwriteCache?: boolean } ``` ### `HashRateLogEntry` Narrowed variant where the hashrate aggregate is present. The dashboard chart components default to this attribute when no `powerAttribute` override is provided. ```typescript type HashRateLogEntry = TailLogEntry & { hashrate_mhs_1m_sum_aggr?: number hashrate_mhs_5m_sum_aggr?: number } ``` ### `HistoricalAlert` A single historical alert row returned by `GET /auth/history-log?logType=alerts`. ```typescript type HistoricalAlert = { uuid?: string name: string description: string message?: string severity: string createdAt: number | string code?: string | number /** Owning device, when th… ``` ### `HistoryLogParams` Query parameters for `GET /auth/history-log` (alerts / info-level history). ```typescript type HistoryLogParams = { logType: 'alerts' | 'info' start?: number end?: number limit?: number offset?: number query?: string } ``` ### `ListRacksParams` Query parameters for `GET /auth/list-racks`. `type` is the worker type (e.g. `miner`, `container`) — omitting it returns `ERR_TYPE_INVALID`. ```typescript type ListRacksParams = { type: string overwriteCache?: boolean } ``` ### `ListThingsDevice` Shape returned by `GET /auth/list-things` for a single device entry. Fields are typed to the union of what the known field projections request (miner explorer, container units, dashboard). Consumers that project fewer fields still satisfy… ```typescript type ListThingsDevice = { id: string type: string status?: string tags?: string[] code?: string rack?: string containerId?: string username?: string address?: string | null err?: stri… ``` ### `ListThingsParams` Query parameters for `GET /auth/list-things`. `query` and `fields` are JSON-stringified Mongo-style selectors. ```typescript type ListThingsParams = { type?: string tag?: string status?: number | string query?: string fields?: string limit?: number offset?: number } ``` ### `LiveAction` A single live action returned by the backend voting queue. ```typescript type LiveAction = { id: string /** The action verb (e.g. `setupPools`, `registerPoolConfig`). */ action?: string type?: string status?: string /** Email/username of the submitte… ``` ### `LiveActionsResponse` Response shape of `GET /auth/actions?queries=…` — a one-element array whose single object maps each requested type to its result list. ```typescript type LiveActionsResponse = { voting?: LiveAction[] ready?: LiveAction[] executing?: LiveAction[] done?: LiveAction[] [key: string]: LiveAction[] | undefined } ``` ### `MinerEntry` A miner row from `GET /auth/miners`, carrying its assigned `poolConfig`. Only the id is guaranteed; the rest is consumed via `lodash.get`. ```typescript type MinerEntry = { id: string [key: string]: unknown } ``` ### `MinerpoolExtDataEntry` Single minerpool ext-data envelope. Backend nests these in `Array<Array<…>>` (per-pool grouping + per-timestamp grouping), so consumers `_head(_head(…))`. ```typescript type MinerpoolExtDataEntry = { ts?: string stats?: PoolMinerStats[] } ``` ### `MinerpoolStatsHistoryEntry` Single sample from the paginated `type=minerpool, key=stats-history` ext-data feed used by the multi-series Hash Rate chart. Each entry carries a timestamp and a snapshot of every configured pool's `hashrate` at that point in time. ```typescript type MinerpoolStatsHistoryEntry = { ts: number stats: PoolMinerStats[] } ``` ### `MinersParams` Query parameters for `GET /auth/miners`. `filter`, `fields`, and `sort` are JSON-stringified Mongo-style selectors; `search` is free text matched across id / code / serial / mac. ```typescript type MinersParams = { filter?: string fields?: string sort?: string search?: string limit?: number offset?: number } ``` ### `MinersResponse` Paginated envelope returned by `GET /auth/miners` — page `data` plus site-wide pagination metadata. ```typescript type MinersResponse = { data: MinerEntry[] totalCount: number offset: number limit: number hasMore: boolean } ``` ### `PduLayoutItem` One PDU row in the static grid layout. `power_w` / `current_a` / `offline` are absent in the static layout and filled from live `pdu_data`. ```typescript type PduLayoutItem = { pdu: string sockets: PduLayoutSocket[] power_w?: number | string current_a?: number | string offline?: boolean } ``` ### `PduLayoutParams` Query parameters for `GET /auth/pdu-layout`. `type` is the full container type string (e.g. `container-bd-d40-m56`). ```typescript type PduLayoutParams = { type: string overwriteCache?: boolean } ``` ### `PduLayoutResponse` Response from `GET /auth/pdu-layout` (verified live 2026-07-01). The backend sources this from the container worker's `pduGridLayout` config, keyed by the exact container type, and 400s with `ERR_PDU_LAYOUT_NOT_FOUND` when no layout is pro… ```typescript type PduLayoutResponse = { type: string layout: PduLayoutItem[] } ``` ### `PduLayoutSocket` One socket in a PDU grid. `enabled` reflects the static layout default; live on/off state comes from the device's `pdu_data` merge. ```typescript type PduLayoutSocket = { socket: string enabled: boolean cooling?: boolean } ``` ### `PoolBalanceHistoryEntry` A single revenue/hashrate sample from `GET /auth/pools/:pool/balance-history`. ```typescript type PoolBalanceHistoryEntry = { ts?: number revenue?: number hashrate?: number /** Settled balance for the bucket (equals `revenue` in the current backend). */ balance?: number [key: string… ``` ### `PoolBalanceHistoryParams` Query parameters for `GET /auth/pools/:pool/balance-history`. The backend requires both `start` and `end` (Unix ms) and rejects the request otherwise; they are typed optional only so a param object can be built incrementally. ```typescript type PoolBalanceHistoryParams = { /** Window start (Unix ms). Required by the backend. */ start?: number /** Window end (Unix ms). Required by the backend. */ end?: number range?: '1D' | '1W'… ``` ### `PoolBalanceHistoryResponse` Response envelope for `GET /auth/pools/:pool/balance-history` — the backend wraps the samples in `{ log }`. ```typescript type PoolBalanceHistoryResponse = { log: PoolBalanceHistoryEntry[] } ``` ### `PoolConfigEntry` Raw pool-configuration row from `GET /auth/configs/pool`. This is the shape the devkit `usePoolConfigs` transform consumes to build a `PoolSummary`. ```typescript type PoolConfigEntry = { id: string poolConfigName: string description: string poolUrls: PoolConfigUrl[] miners: number containers: number updatedAt: string | number } ``` ### `PoolConfigForDeviceResponse` Response for `GET /auth/pools/config/:id` — the device's assigned pool config id and the count of miners overriding their container's config. ```typescript type PoolConfigForDeviceResponse = { poolConfig: string | null overriddenConfig: number } ``` ### `PoolConfigUrl` A single pool-URL endpoint as stored on a pool configuration. `url` is a `stratum+tcp://host:port` string; the devkit `usePoolConfigs` transform parses it into host/port/role for display. ```typescript type PoolConfigUrl = { url: string pool: string workerName?: string workerPassword?: string } ``` ### `PoolMinerStats` Per-pool stats entry returned by `GET /auth/ext-data?type=minerpool`. Each configured pool (`f2pool`, `ocean`, …) contributes one row. ```typescript type PoolMinerStats = { poolType?: string /** Subaccount / user the pool worker submits shares under. */ username?: string /** * Pool-reported hashrate in **H/s** (raw hashes per se… ``` ### `PoolsResponse` Response envelope for `GET /auth/pools` — the aggregated `pools` list plus a site-wide `summary`. The backend wraps the array, so consumers must read `.pools` rather than treating the payload as an array. ```typescript type PoolsResponse = { pools: AggregatedPool[] summary: PoolsSummary } ``` ### `PoolsSummary` Site-wide totals returned alongside the pool list by `GET /auth/pools`. ```typescript type PoolsSummary = { poolCount: number totalHashrate: number totalWorkers: number totalBalance: number } ``` ### `PowerModeTimelineEntry` Power-mode timeline entry — carries grouped per-miner mode/status maps keyed by miner id. Consumed by `PowerModeTimelineChart`. ```typescript type PowerModeTimelineEntry = TailLogEntry & { power_mode_group_aggr?: Record status_group_aggr?: Record } ``` ### `Rack` A single rack entry from `GET /auth/list-racks`. Typed loosely — the `listRacks` RPC response shape has not been captured against a live backend yet (staging returned no rack data at verification time). ```typescript type Rack = { id?: string name?: string [key: string]: unknown } ``` ### `SiteResponse` Response from `GET /auth/site` — the configured site label. ```typescript type SiteResponse = { site: string } ``` ### `SiteStatusAlerts` Alert counts by severity from the live site-status snapshot. ```typescript type SiteStatusAlerts = { critical: number high: number medium: number total: number } ``` ### `SiteStatusLive` Composite live site-status snapshot from `GET /auth/site/status/live`. Aggregates site-wide hashrate, power, efficiency, miner/alert/pool counts, and the snapshot timestamp (`ts`, Unix ms). Polled on a short interval. ```typescript type SiteStatusLive = { hashrate: SiteStatusMetric power: SiteStatusPower efficiency: SiteStatusMetric miners: SiteStatusMiners alerts: SiteStatusAlerts pools: SiteStatusPools /** S… ``` ### `SiteStatusMetric` A `{ value, unit }` measurement, optionally annotated with a `nominal` (rated) value and a `utilization` percentage (`value / nominal * 100`). ```typescript type SiteStatusMetric = { value: number unit: string nominal?: number utilization?: number } ``` ### `SiteStatusMiners` Miner population counts from the live site-status snapshot. ```typescript type SiteStatusMiners = { online: number offline: number error: number total: number containerCapacity: number } ``` ### `SiteStatusPools` Pool-side aggregates from the live site-status snapshot. ```typescript type SiteStatusPools = { totalHashrate: { value: number; unit: string } activeWorkers: number totalWorkers: number } ``` ### `SiteStatusPower` Power metric from the live site-status snapshot. Like `SiteStatusMetric` but with an `alert` message and a hard `error` flag. ```typescript type SiteStatusPower = SiteStatusMetric & { alert?: string error?: boolean } ``` ### `SubmitBatchActionsPayload` Body for `POST /auth/actions/voting/batch` — a set of staged actions plus a client-generated `batchActionUID` the backend uses to group them. ```typescript type SubmitBatchActionsPayload = { batchActionsPayload: VotingActionPayload[] batchActionUID: string suffix?: string /** Batch-level annotations (e.g. `{ isBackFromMaintenance }` on miner move… ``` ### `TailLogEntry` A single time-bucketed entry from `GET /auth/tail-log`. Backend returns `Array<Array<TailLogEntry>>` (outer wrapping is the per-worker grouping — single-site dashboards take `_head(response)`). ```typescript type TailLogEntry = { ts: number /** All other fields are dynamic aggregates requested via `aggrFields`. */ [key: string]: unknown } ``` ### `TailLogMultiParams` Query parameters for `GET /auth/tail-log/multi` — the batched variant of tail-log. `keys` is required (comma-separated `stat-*` keys); the rest mirrors the Fastify schema in `miningos-gateway`. {todo: update miningos-gateway} ```typescript type TailLogMultiParams = { keys: string start?: number end?: number offset?: number limit?: number fields?: string aggrFields?: string aggrTimes?: string overwriteCache?: boolean } ``` ### `TailLogParams` Query parameters for `GET /auth/tail-log`. Mirrors the Fastify schema in `miningos-gateway`. `aggrFields` is a JSON-stringified object describing which aggregate columns to include in each row. ```typescript type TailLogParams = { key: string type?: string tag?: string start?: number end?: number offset?: number limit?: number fields?: string aggrFields?: string aggrTimes?: string merg… ``` ### `ThingCommentBody` Body for `POST | PUT | DELETE /auth/thing/comment` (add / edit / delete a device comment — one Fastify schema covers all three verbs). `rackId` + `thingId` + `comment` are required; `id` and `ts` identify an existing comment for edit/delet… ```typescript type ThingCommentBody = { rackId: string thingId: string comment: string /** Existing comment id — required when editing or deleting. */ id?: string pos?: string ts?: number } ``` ### `ThingConfigParams` Query parameters for `GET /auth/thing-config` — both fields are required by the Fastify schema. ```typescript type ThingConfigParams = { type: string requestType: string } ``` ### `VoteActionPayload` Body for `PUT /auth/actions/voting/:id/vote`. ```typescript type VoteActionPayload = { id: string | number approve: boolean } ``` ### `VotingActionObjectParam` Object-shaped `params[]` entry on a voting submission. Pool create/update carry `{ type: 'pool', data }` (+ `id` for updates); assign-pool carries `{ poolConfigId, configType: 'pool' }`; `switchSocket` carries `{ pdu, socket, enabled }`; `… ```typescript type VotingActionObjectParam = { type?: string id?: string poolConfigId?: string configType?: string data?: Record [key: string]: unknown } ``` ### `VotingActionParam` A single `params[]` entry on a voting submission. Positional and action-specific: device actions carry primitives (`setPowerMode` sends `['sleep']`, `setLED` sends `[true]`, `setTankEnabled` sends `[3, true]`), pool/thing actions carry {@l… ```typescript type VotingActionParam = string | number | boolean | VotingActionObjectParam ``` ### `VotingActionPayload` Body for `POST /auth/actions/:type` (default `voting`). `type` selects the URL segment and is stripped from the JSON body before posting — the remaining fields (`query`, `action`, `params`, `rackType`, …) form the request body. ```typescript type VotingActionPayload = { /** URL segment under `/auth/actions/:type`. Defaults to `voting`. */ type?: string query?: Record action?: string params?: VotingActionPara… ``` # Card Types (/reference/ui/types/cards) Type definitions for card components. ## Package `@tetherto/mdk-react-devkit` ## Types @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `MiningPoolRow` Display contract for one row of the `<MiningPoolsPanel />` table. Adapter hooks (e.g. `usePoolRows`) shape their output to fit this type; consumers can also build rows by hand for fixtures or demos. ```typescript type MiningPoolRow = { /** Stable React key. */ id: string /** Display name (e.g. `minerpool-f2pool-shelf-0`). */ name: string /** 24h revenue in BTC; rendered as `0.00 BTC` when undefined. */ revenue24hBtc?: number /** Hashrate in PH/s — used for display (TH/… ``` # Devkit General Types (/reference/ui/types/devkit-general) General type definitions from the react-devkit package. ## Package `@tetherto/mdk-react-devkit` ## Types @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActionButtonConfirmation` ```typescript type ActionButtonConfirmation = { title: string cancelLabel?: string confirmLabel?: string icon?: React.ReactNode onCancel?: VoidFunction onConfirm?: VoidFunction description?: React.ReactNode } ``` ### `ActionButtonProps` ```typescript type ActionButtonProps = { label?: string loading?: boolean disabled?: boolean className?: string variant?: TActionButtonVariant confirmation: ActionButtonConfirmation /** Confirmation mode: popover (inline) or dialog (modal). Default: popover */ mode?: 'popover'… ``` ### `ActionNameKey` ```typescript type ActionNameKey = keyof typeof ACTION_NAMES_MAP ``` ### `ActionNameValue` ```typescript type ActionNameValue = (typeof ACTION_NAMES_MAP)[ActionNameKey] ``` ### `ActionStatusTypeKey` ```typescript type ActionStatusTypeKey = keyof typeof ACTION_STATUS_TYPES ``` ### `ActionStatusTypeValue` ```typescript type ActionStatusTypeValue = (typeof ACTION_STATUS_TYPES)[ActionStatusTypeKey] ``` ### `ActionTypeKey` ```typescript type ActionTypeKey = keyof typeof ACTION_TYPES ``` ### `ActionTypeValue` ```typescript type ActionTypeValue = (typeof ACTION_TYPES)[ActionTypeKey] ``` ### `ActiveIncidentsCardPartialProps` ```typescript type ActiveIncidentsCardPartialProps = Partial<{ label: string isLoading: boolean className: string skeletonRows: number emptyMessage: string items: TIncidentRowProps[] onItemClick: (id: string) => void }> ``` ### `ActiveTimeframe` Which single control “owns” the range: others show placeholders (mutually exclusive UI). ```typescript type ActiveTimeframe = 'year' | 'month' | 'week' ``` ### `ActualEbitdaCardProps` ```typescript type ActualEbitdaCardProps = { value: number } ``` ### `AddSparePartFormValues` Values collected by the register-part form. Select fields are nullable until chosen. ```typescript type AddSparePartFormValues = { partTypeId: string; model: string | null; parentDeviceModel: string | null; serialNum: string; macAddress: string; status: string | null; location: string | null; comment: string; tags: string[]; } ``` ### `AddSparePartModalProps` Props for `AddSparePartModal`; option lists and handlers are supplied by the caller. ```typescript type AddSparePartModalProps = { isOpen: boolean; onClose: VoidFunction; partTypes: SparePartSubTypesModalPartType[]; defaultPartTypeId?: string; modelOptions: FormSelectOption[]; isModelOptionsLoading?: boolean; minerModelOptions: FormSelectOption[]; statusOptions: For… ``` ### `AddUserModalProps` ```typescript type AddUserModalProps = { open: boolean onClose: VoidFunction roles: RoleOption[] onSubmit: (data: { name: string; email: string; role: string }) => Promise isSubmitting?: boolean } ``` ### `AlarmItemData` ```typescript type AlarmItemData = { title: string subtitle: string body: string uuid?: string status: string [key: string]: unknown } ``` ### `AlarmsBellButtonCounts` ```typescript type AlarmsBellButtonCounts = { critical?: number high?: number medium?: number } ``` ### `AlarmsBellButtonProps` ```typescript type AlarmsBellButtonProps = { /** Severity-bucketed alarm counts rendered in the stacked badge. */ counts?: AlarmsBellButtonCounts /** Click handler — typically opens an alerts panel or routes to /alerts. */ onClick?: (event: MouseEvent) => void /*… ``` ### `Alert` ```typescript type Alert = { id?: string severity: string createdAt: number | string name: string description: string message?: string uuid?: string code?: string | number [key: string]: unknown } ``` ### `AlertActions` ```typescript type AlertActions = { onAlertClick?: (id: string, uuid: string) => void id?: string uuid: string } ``` ### `AlertConfirmationModalProps` ```typescript type AlertConfirmationModalProps = { isOpen: boolean onOk: VoidFunction } ``` ### `AlertLocalFilters` ```typescript type AlertLocalFilters = { severity?: string[] | string status?: string[] type?: string[] id?: string[] thing?: { id?: string } [key: string]: unknown } ``` ### `AlertSeverity` ```typescript type AlertSeverity = (typeof ALERT_SEVERITY_TYPES)[keyof typeof ALERT_SEVERITY_TYPES] ``` ### `AlertsProps` ```typescript type AlertsProps = { /** * Devices payload powering the "Current Alerts" table. * Mirrors the API response shape of `useGetListThingsQuery({ ... alerts query })`. */ devices?: Device[][] /** * Loading flag for the "Current Alerts" table. */ isCurrentAlertsLo… ``` ### `AlertsTableTitleProps` ```typescript type AlertsTableTitleProps = { title: ReactNode subtitle?: ReactNode className?: string } ``` ### `AlertTableRecord` ```typescript type AlertTableRecord = { shortCode: string device: string alertName: string description?: string message?: string severity: string createdAt: number | string id?: string uuid: string actions?: AlertActions /** * Hidden tokens (uuid, ip-/mac-/sn-/firmware- tags,… ``` ### `ApiErrorKey` ```typescript type ApiErrorKey = keyof typeof API_ERRORS ``` ### `ApiErrorValue` ```typescript type ApiErrorValue = (typeof API_ERRORS)[ApiErrorKey] ``` ### `ApiResponse` ```typescript type ApiResponse = { data: T message?: string status: number } ``` ### `AppHeaderProps` ```typescript type AppHeaderProps = { /** Left-most slot — typically the app's brand lockup / logo. */ logo?: ReactNode /** Left-edge content — e.g. a sidebar collapse toggle button. */ start?: ReactNode /** Middle slot — typically the dashboard's stats strip. */ children?:… ``` ### `AreaChartProps` ```typescript type AreaChartProps = { /** Chart data - required, provided by parent */ data: ChartJS<'line'>['data'] /** Chart.js options - merged with defaults */ options?: ChartJS<'line'>['options'] /** Custom HTML tooltip configuration. When provided, replaces the default… ``` ### `ArrowIconProps` ```typescript type ArrowIconProps = { isOpen?: boolean } & IconProps ``` ### `AssignPoolModalProps` ```typescript type AssignPoolModalProps = { isOpen: boolean onClose: () => void onSubmit: (values: { pool: PoolSummary }) => Promise miners: Device[] poolConfig: PoolConfigData[] } ``` ### `AttentionLevelColorKey` ```typescript type AttentionLevelColorKey = keyof typeof ATTENTION_LEVEL_COLORS ``` ### `AuthCapKey` ```typescript type AuthCapKey = keyof typeof AUTH_CAPS ``` ### `AuthCapValue` ```typescript type AuthCapValue = (typeof AUTH_CAPS)[AuthCapKey] ``` ### `AverageDowntimeChartData` ```typescript type AverageDowntimeChartData = { labels: string[] curtailment?: number[] operationalIssues?: number[] } ``` ### `AverageDowntimeChartProps` ```typescript type AverageDowntimeChartProps = Partial<{ title: string unit: string height: number barWidth: number className: string isLoading: boolean emptyMessage: string data: AverageDowntimeChartData /** Formats Y-axis ticks, tooltips, and bar data labels (values are 0–1 rates). *… ``` ### `AvgAllInCostChartProps` ```typescript type AvgAllInCostChartProps = { data?: ReadonlyArray dateRange: FinancialDateRange | null isLoading?: boolean } ``` ### `AvgAllInCostDataPoint` ```typescript type AvgAllInCostDataPoint = { ts: number revenueUSDPerMWh: number costUSDPerMWh: number } ``` ### `AvgHistoryRangeKey` ```typescript type AvgHistoryRangeKey = keyof typeof AVG_HISTORY_RANGES ``` ### `AvgHistoryRangeValue` ```typescript type AvgHistoryRangeValue = (typeof AVG_HISTORY_RANGES)[AvgHistoryRangeKey] ``` ### `AxisTitleText` ```typescript type AxisTitleText = { x: string y: string } ``` ### `BadgeColors` Display colors for a status/location badge. ```typescript type BadgeColors = { backgroundColor: string borderColor: string } ``` ### `BadgeProps` ```typescript type BadgeProps = { /** * Badge content (wraps children with badge) */ children?: ReactNode /** * Number to display in badge * If > overflowCount, will show "overflowCount+" */ count?: number /** * Maximum count to display * @default 99 */ overflowCount?: n… ``` ### `BadgeStatus` Badge status options ```typescript type BadgeStatus = 'success' | 'processing' | 'error' | 'warning' | 'default' ``` ### `BarChartConstant` ```typescript type BarChartConstant = { label: string value: number color?: string borderDash?: number[] yAxisID?: string } ``` ### `BarChartInput` ```typescript type BarChartInput = { labels?: string[] series: BarChartSeries[] lines?: BarChartLine[] constants?: BarChartConstant[] } ``` ### `BarChartItemBorderColorKey` ```typescript type BarChartItemBorderColorKey = keyof typeof BAR_CHART_ITEM_BORDER_COLORS ``` ### `BarChartLine` ```typescript type BarChartLine = { label: string values: number[] | Record color?: string yAxisID?: string tension?: number pointRadius?: number pointHoverRadius?: number } ``` ### `BarChartProps` ```typescript type BarChartProps = { /** Chart data - required, provided by parent. Use `as any` for mixed bar+line datasets. */ data: any /** Chart.js options - merged with defaults */ options?: ChartJS<'bar'>['options'] /** Stack bars on top of each other */ isStacked?: b… ``` ### `BarChartSeries` ```typescript type BarChartSeries = { label: string values: number[] | Record color?: string /** Assign a stack group id to stack multiple series together */ stack?: string /** Gradient opacity stops (default { top: 0.3, bottom: 0.1 }) */ gradient?: { top: nu… ``` ### `BatchActionTypeKey` ```typescript type BatchActionTypeKey = keyof typeof BATCH_ACTION_TYPES ``` ### `BatchActionTypeValue` ```typescript type BatchActionTypeValue = (typeof BATCH_ACTION_TYPES)[BatchActionTypeKey] ``` ### `BatchMoveSparePart` A spare part shown in the batch-move table. ```typescript type BatchMoveSparePart = { id: string; code: string; location: string; status: string; } ``` ### `BatchMoveSparePartsModalProps` Props for `BatchMoveSparePartsModal`; unselected location/status arrive as `null` on submit. ```typescript type BatchMoveSparePartsModalProps = { isOpen: boolean; onClose: VoidFunction; spareParts: BatchMoveSparePart[]; locationOptions: FormSelectOption[]; statusOptions: FormSelectOption[]; onSubmit: (values: { location: string | null; status: string | null; observation: string |… ``` ### `BitcoinPriceCardProps` ```typescript type BitcoinPriceCardProps = { value: number } ``` ### `BitcoinProducedCardProps` ```typescript type BitcoinProducedCardProps = { value: number } ``` ### `BitcoinProducedChartProps` ```typescript type BitcoinProducedChartProps = { chartData: BarChartDataResult isLoading?: boolean hasAllZeros?: boolean height?: number } ``` ### `BitcoinProductionCostCardProps` ```typescript type BitcoinProductionCostCardProps = { value: number } ``` ### `BitMainControlsTabProps` ```typescript type BitMainControlsTabProps = { /** Device data */ data: Device } ``` ### `BitMainHydroSettingsProps` ```typescript type BitMainHydroSettingsProps = { /** Device data */ data?: Device } ``` ### `BitMainImmersionSummaryBoxContainerSettings` ```typescript type BitMainImmersionSummaryBoxContainerSettings = { thresholds?: Record } ``` ### `BitMainImmersionSummaryBoxProps` ```typescript type BitMainImmersionSummaryBoxProps = { data?: Device containerSettings?: BitMainImmersionSummaryBoxContainerSettings | null } ``` ### `BorderRadius` Border radius variants Used by Checkbox, Switch, Radio ```typescript type BorderRadius = 'none' | 'small' | 'medium' | 'large' | 'full' ``` ### `BreadcrumbItem` ```typescript type BreadcrumbItem = { label: string href?: string onClick?: VoidFunction } ``` ### `BreadcrumbsProps` ```typescript type BreadcrumbsProps = { items: BreadcrumbItem[] showBack?: boolean backLabel?: string className?: string itemClassName?: string backClassName?: string onBackClick?: VoidFunction separator?: ReactNode } ``` ### `BtcAveragePriceProps` ```typescript type BtcAveragePriceProps = Partial<{ /** * BTC price in USD; formatted with grouping and no decimal places. * When `null`, `undefined`, non-finite, or negative, the value shows `-` (`FALLBACK` from format utils). */ price: number | null /** * Label for the BTC avera… ``` ### `BtcPriceTimeSeriesEntry` ```typescript type BtcPriceTimeSeriesEntry = { ts: number priceUSD: number } ``` ### `BuildAvgAllInCostInputArgs` ```typescript type BuildAvgAllInCostInputArgs = { data?: ReadonlyArray dateRange: FinancialDateRange | null } ``` ### `BuildBarChartOptionsInput` ```typescript type BuildBarChartOptionsInput = { isHorizontal?: boolean isStacked?: boolean yTicksFormatter?: (value: number) => string yRightTicksFormatter?: ((value: number) => string) | null displayColors?: boolean legendPosition?: 'top' | 'bottom' | 'left' | 'right' legendAlign?: '… ``` ### `BuildProductionCostInputArgs` ```typescript type BuildProductionCostInputArgs = { costLog: ReadonlyArray btcPriceLog: ReadonlyArray dateRange: FinancialDateRange | null } ``` ### `BulkAddSparePartsModalProps` Props for `BulkAddSparePartsModal`; `onSubmit` receives the parsed CSV records. ```typescript type BulkAddSparePartsModalProps = { isOpen: boolean; onClose: VoidFunction; onSubmit: (records: CSVRecord[]) => Promise<{ error?: string } | void>; isLoading?: boolean; } ``` ### `ButtonIconPosition` Button icon position ```typescript type ButtonIconPosition = 'left' | 'right' ``` ### `ButtonProps` Props for `Button`. Extends all native `<button>` attributes. ```typescript type ButtonProps = Partial< { /** Show a spinner instead of the content and disable the button. */ loading: boolean /** Make the button stretch to fill its container. */ fullWidth: boolean /** Icon node rendered alongside `children`. */ icon: ReactNode /** V… ``` ### `ButtonSize` Button-specific size including icon-only variant ```typescript type ButtonSize = ComponentSize | 'icon' ``` ### `ButtonVariant` Component-specific prop types ```typescript type ButtonVariant = | 'primary' | 'secondary' | 'danger' | 'tertiary' | 'link' | 'nav-link' | 'icon' | 'outline' | 'ghost' ``` ### `CabinetDetailCardProps` ```typescript type CabinetDetailCardProps = { /** Cabinet display title (`LV Cabinet 1` / transformer title). */ title: string /** Non-root powermeter reading rows. */ powerMeters: CabinetReadingRow[] /** The cabinet-root temperature reading, when present. */ rootTempSensor?: Cabine… ``` ### `CabinetReadingRow` One sensor / powermeter reading row rendered in the cabinet detail. ```typescript type CabinetReadingRow = { /** Stable key — the source device id. */ id: string /** Display name (sensor / powermeter name). */ label: string /** Formatted reading, or `-` when unavailable. */ value: string /** Unit shown next to the value. */ unit: string /** Sev… ``` ### `CardBodyProps` Props for `CardBody` slot. Forwards all native `<div>` attributes. ```typescript type CardBodyProps = HTMLAttributes ``` ### `CardComponent` ```typescript type CardComponent = ForwardRefExoticComponent> & { Header: typeof CardHeader Body: typeof CardBody Footer: typeof CardFooter } ``` ### `CardFooterProps` Props for `CardFooter` slot. Forwards all native `<div>` attributes. ```typescript type CardFooterProps = HTMLAttributes ``` ### `CardHeaderProps` Props for `CardHeader` slot. Forwards all native `<div>` attributes. ```typescript type CardHeaderProps = HTMLAttributes ``` ### `CardProps` Props for `Card`. Forwards all native `<div>` attributes. ```typescript type CardProps = HTMLAttributes ``` ### `CascaderOption` Cascader option type - represents a hierarchical option with optional children ```typescript type CascaderOption = { /** Unique value for the option */ value: string | number | boolean /** Display label for the option */ label: string /** Child options (creates a nested category) */ children?: CascaderOption[] /** Whether the option is disabled */ disa… ``` ### `CascaderProps` Cascader component props ```typescript type CascaderProps = { /** * Hierarchical options to display in the cascader * Parent options with children appear in the left panel * Child options appear in the right panel when parent is selected */ options: CascaderOption[] /** * Current selected value(s)… ``` ### `CascaderValue` Cascader value type - array representing the path from parent to child Example: ['electronics', 'phones'] means category 'electronics', option 'phones' ```typescript type CascaderValue = (string | number | boolean)[] ``` ### `ChangeConfirmationModalProps` ```typescript type ChangeConfirmationModalProps = { open: boolean title: string onConfirm: VoidFunction onClose: VoidFunction children: ReactNode confirmText?: string destructive?: boolean } ``` ### `ChartColorKey` ```typescript type ChartColorKey = keyof typeof CHART_COLORS ``` ### `ChartContainerProps` ```typescript type ChartContainerProps = { title?: string /** * Optional node rendered immediately after the title text (e.g. an info * tooltip). Only shown when `title` is set and `header` is not. Additive - * omit it and the title renders exactly as before. */ titleExtra?: Reac… ``` ### `ChartDataByDevice` ```typescript type ChartDataByDevice = { [deviceName: string]: ChartDataset } ``` ### `ChartDataPoint` ```typescript type ChartDataPoint = { x: number | string y: number } ``` ### `ChartEntry` ```typescript type ChartEntry = { ts: number | string container_specific_stats_group_aggr: Record } ``` ### `ChartExpandActionProps` ```typescript type ChartExpandActionProps = { /** Whether the parent chart is currently expanded to full width. */ isExpanded: boolean /** Toggles the expanded state. */ onToggle?: VoidFunction } ``` ### `ChartLabelKey` ```typescript type ChartLabelKey = keyof typeof CHART_LABELS ``` ### `ChartLabelValue` ```typescript type ChartLabelValue = (typeof CHART_LABELS)[ChartLabelKey] ``` ### `ChartLegendOpacityKey` ```typescript type ChartLegendOpacityKey = keyof typeof CHART_LEGEND_OPACITY ``` ### `ChartLegendOpacityValue` ```typescript type ChartLegendOpacityValue = (typeof CHART_LEGEND_OPACITY)[ChartLegendOpacityKey] ``` ### `ChartLegendPosition` ```typescript type ChartLegendPosition = 'top' | 'bottom' | 'left' | 'right' | 'center' | 'chartArea' ``` ### `ChartPerformanceKey` ```typescript type ChartPerformanceKey = keyof typeof CHART_PERFORMANCE ``` ### `ChartRange` ```typescript type ChartRange = { min: Date | number max: Date | number } ``` ### `ChartStatsFooterItem` ```typescript type ChartStatsFooterItem = { label: string value: string | number } ``` ### `ChartStatsFooterProps` ```typescript type ChartStatsFooterProps = Partial<{ /** Min/Max/Avg values row */ minMaxAvg: MinMaxAvgValues /** Additional stats displayed in a columnar grid */ stats: ChartStatsFooterItem[] /** Number of stat items per column (default: 1) */ statsPerColumn: number /** Secondary… ``` ### `ChartTitleKey` ```typescript type ChartTitleKey = keyof typeof CHART_TITLES ``` ### `ChartTitleValue` ```typescript type ChartTitleValue = (typeof CHART_TITLES)[ChartTitleKey] ``` ### `ChartTooltipConfig` ```typescript type ChartTooltipConfig = { /** Color for label text. Use 'dataset' to match dataset border color. Defaults to COLOR.WHITE_ALPHA_05 */ labelColor?: string | 'dataset' /** Color for value text. Use 'dataset' to match dataset border color. Defaults to 'dataset' */ va… ``` ### `ChartTypeKey` ```typescript type ChartTypeKey = keyof typeof CHART_TYPES ``` ### `ChartTypeValue` ```typescript type ChartTypeValue = (typeof CHART_TYPES)[ChartTypeKey] ``` ### `CheckboxProps` ```typescript type CheckboxProps = { /** * Size variant of the checkbox * @default 'md' */ size?: CheckboxSize /** * Color variant when checked * @default 'primary' */ color?: ComponentColor /** * Border radius variant * @default 'none' */ radius?: BorderRadius /** * Custom… ``` ### `CheckboxSize` ```typescript type CheckboxSize = 'xs' | ComponentSize ``` ### `ChipData` ```typescript type ChipData = { index: number current: number } ``` ### `ColorKey` ```typescript type ColorKey = keyof typeof COLOR ``` ### `ColorValue` ```typescript type ColorValue = (typeof COLOR)[ColorKey] ``` ### `ColorVariant` Color variants for components Comprehensive set including all semantic colors ```typescript type ColorVariant = | 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' ``` ### `ComponentColor` Component color options (subset of ColorVariant) Used by form components like Checkbox, Switch, Radio, Typography ```typescript type ComponentColor = 'default' | 'primary' | 'success' | 'warning' | 'error' ``` ### `ComponentSize` Common shared types used across components ```typescript type ComponentSize = 'sm' | 'md' | 'lg' ``` ### `ConfirmDeleteSparePartModalProps` Props for `ConfirmDeleteSparePartModal`. ```typescript type ConfirmDeleteSparePartModalProps = { isOpen?: boolean; onClose?: VoidFunction; onConfirm?: (sparePart: ConfirmDeleteSparePartModalSparePart) => Promise | void; sparePart?: ConfirmDeleteSparePartModalSparePart; isLoading?: boolean; } ``` ### `ConfirmDeleteSparePartModalSparePart` The spare part targeted for deletion. ```typescript type ConfirmDeleteSparePartModalSparePart = { id: string; code: string; } ``` ### `ConsumptionNominalValueType` ```typescript type ConsumptionNominalValueType = typeof CONSUMPTION_NOMINAL_VALUE_W ``` ### `Container` ```typescript type Container = { info?: Partial last?: Partial } & Device ``` ### `ContainerActionValue` ```typescript type ContainerActionValue = (typeof CONTAINER_ACTIONS)[number] ``` ### `ContainerActivityData` Miner-state counts fed to the embedded activity chart (online / offline / faulted / …). ```typescript type ContainerActivityData = { total?: number } & Record ``` ### `ContainerActivityError` Error envelope surfaced by the activity chart when its data fails to load. ```typescript type ContainerActivityError = { data?: { message?: string }; [key: string]: unknown } | null ``` ### `ContainerChartCombinationOption` ```typescript type ContainerChartCombinationOption = { value: string label: string } ``` ### `ContainerChartsDatasetBorderColorResolver` ```typescript type ContainerChartsDatasetBorderColorResolver = (args: { chartTitle: string datasetLabel: string datasetIndex: number }) => string | undefined ``` ### `ContainerChartsProps` ```typescript type ContainerChartsProps = { /** When false, shows an empty state (feature gate). @default true */ featureEnabled?: boolean /** Message when `featureEnabled` is false */ disabledMessage?: string /** Options for the combination selector */ combinations: ContainerChar… ``` ### `ContainerControlsBoxProps` ```typescript type ContainerControlsBoxProps = { data?: Device isBatch?: boolean isCompact?: boolean // --- data from outside (no API calls inside) --- selectedDevices?: Device[] pendingSubmissions?: PendingSubmission[] alarmsDataItems?: TimelineItemData[] tailLogData?: UnknownRecord[]… ``` ### `ContainerDetailPlaceholderProps` ```typescript type ContainerDetailPlaceholderProps = { /** Human label of the tab this placeholder stands in for, e.g. "PDU Layout". */ label: string } ``` ### `ContainerDetailProps` ```typescript type ContainerDetailProps = { /** * Container display name shown in the header. Optional — omit it when the * host already renders the container name as the page title (e.g. the shell's * `PageLayout`), so the name is not shown twice. */ name?: ReactNode /** Ordered… ``` ### `ContainerDetailTab` A single tab in the container detail strip. ```typescript type ContainerDetailTab = { /** Route segment / Radix value, e.g. `"home"`, `"pdu"`. */ key: string /** Human label, e.g. `"PDU Layout"`. */ label: string } ``` ### `ContainerInfo` ```typescript type ContainerInfo = { container: string type?: string rack?: string nominalMinerCapacity?: string cooling_system: Record cdu: Record primary_supply_temp: number second_supply_temp1: number second_supply_temp2: number supply_l… ``` ### `ContainerLast` ```typescript type ContainerLast = { snap: { stats?: Partial } alerts: unknown[] | null err: string | null } ``` ### `ContainerPduData` ```typescript type ContainerPduData = { power_w: number status: number } ``` ### `ContainerPosInfo` ```typescript type ContainerPosInfo = { containerInfo: Partial pdu: string | number socket: string | number pos: string [key: string]: unknown } ``` ### `ContainerSnap` ```typescript type ContainerSnap = { stats?: Partial config?: Record } ``` ### `ContainerSpecific` ```typescript type ContainerSpecific = { pdu_data: Partial[] [key: string]: unknown } ``` ### `ContainerStats` ```typescript type ContainerStats = { status: string ambient_temp_c: number humidity_percent: number power_w: number container_specific: Partial distribution_box1_power_w: number distribution_box2_power_w: number stats: Record temperature_… ``` ### `ContainerWidgetCardProps` ```typescript type ContainerWidgetCardProps = { /** Container display name shown in the header row. */ title: string /** Latest container power draw in watts (rendered in kW by the top row). */ power?: number /** Power unit label shown next to the reading. */ powerUnit?: string /** Pe… ``` ### `ContainerWidgetItem` One container's card data plus the stable id used for keys and click routing. ```typescript type ContainerWidgetItem = ContainerWidgetCardProps & { id: string } ``` ### `ContainerWidgetsProps` ```typescript type ContainerWidgetsProps = { /** Card-ready data for every container, shaped by the data hook. */ containers: ContainerWidgetItem[] /** Section heading. */ title?: string /** Shows a spinner while the first load is in flight. */ isLoading?: boolean /** Error message… ``` ### `CostChartsProps` ```typescript type CostChartsProps = { costLog: ReadonlyArray btcPriceLog: ReadonlyArray totals: CostSummaryMonetaryTotals | null dateRange: FinancialDateRange | null avgAllInCostData?: ReadonlyArray isLoadi… ``` ### `CostChromeProps` ```typescript type CostChromeProps = { /** Period selector element. Pass `` for the OSS-style year/month picker. */ controls: ReactElement /** * Optional "Set Monthly Cost" header action slot. * * A `ReactElement` slot (rather than an href string) so consum… ``` ### `CostContentProps` ```typescript type CostContentProps = CostViewModelProps & CostQueryStateProps & { dateRange: FinancialDateRange | null /** Optional revenue/cost time-series for the Avg All-in Cost panel. */ avgAllInCostData?: ReadonlyArray } ``` ### `CostMetricsProps` ```typescript type CostMetricsProps = { metrics: CostSummaryDisplayMetrics } ``` ### `CostProps` ```typescript type CostProps = CostContentProps & CostChromeProps ``` ### `CostQueryStateProps` ```typescript type CostQueryStateProps = { isLoading?: boolean error?: unknown } ``` ### `CostSummaryDisplayMetrics` ```typescript type CostSummaryDisplayMetrics = { allInCost: CostSummaryMetric energyCost: CostSummaryMetric operationsCost: CostSummaryMetric } ``` ### `CostSummaryLogEntry` ```typescript type CostSummaryLogEntry = { ts: number consumptionMWh: number energyCostsUSD: number operationalCostsUSD: number totalCostsUSD: number allInCostPerMWh: number | null energyCostPerMWh: number | null btcPrice: number } ``` ### `CostSummaryMetric` ```typescript type CostSummaryMetric = { label: string unit: string value: number | null isHighlighted?: boolean } ``` ### `CostSummaryMonetaryTotals` ```typescript type CostSummaryMonetaryTotals = { totalEnergyCostsUSD: number totalOperationalCostsUSD: number totalCostsUSD: number totalConsumptionMWh: number } ``` ### `CostSummaryQueryParams` ```typescript type CostSummaryQueryParams = { start: number end: number period: FinancePeriod } ``` ### `CostSummaryQueryResult` ```typescript type CostSummaryQueryResult = { data?: CostSummaryResponse isLoading?: boolean error?: unknown } ``` ### `CostSummaryResponse` ```typescript type CostSummaryResponse = FinanceResponse ``` ### `CostSummaryTotals` ```typescript type CostSummaryTotals = { totalEnergyCostsUSD: number totalOperationalCostsUSD: number totalCostsUSD: number totalConsumptionMWh: number avgAllInCostPerMWh: number | null avgEnergyCostPerMWh: number | null avgBtcPrice: number | null } ``` ### `CostSummaryViewModel` ```typescript type CostSummaryViewModel = { metrics: CostSummaryDisplayMetrics | null costLog: ReadonlyArray btcPriceLog: ReadonlyArray totals: CostSummaryMonetaryTotals | null avgBtcPrice: number | null } ``` ### `CostTimeSeriesEntry` ```typescript type CostTimeSeriesEntry = { ts: number totalCostUSD: number energyCostUSD: number operationalCostUSD: number } ``` ### `CostViewModelProps` Subset of `useCostSummary` output that the Cost composite page needs. Defined as a standalone type so consumers wiring up their own data flow can pass an object of this shape without coupling to the hook's full return. ```typescript type CostViewModelProps = { metrics: CostSummaryDisplayMetrics | null costLog: ReadonlyArray btcPriceLog: ReadonlyArray totals: CostSummaryMonetaryTotals | null } ``` ### `CreateIconOptions` ```typescript type CreateIconOptions = { displayName: string viewBox: string defaultWidth?: number defaultHeight?: number multiColor?: boolean path: ReactNode | ((props: { color: string }) => ReactNode) } ``` ### `CrossThingTypeKey` ```typescript type CrossThingTypeKey = keyof typeof CROSS_THING_TYPES ``` ### `CrossThingTypeValue` ```typescript type CrossThingTypeValue = (typeof CROSS_THING_TYPES)[CrossThingTypeKey] ``` ### `CSVRecord` ```typescript type CSVRecord = { partType?: string; model?: string; parentDeviceModel?: string; serialNum?: string; macAddress?: string; status?: string; location?: string; comment?: string; rackId?: string; [key: string]: unknown; } ``` ### `CurrencyKey` ```typescript type CurrencyKey = keyof typeof CURRENCY ``` ### `CurrencyUnit` ```typescript type CurrencyUnit = ValueUnit & { unit: string } ``` ### `CurrencyValue` ```typescript type CurrencyValue = (typeof CURRENCY)[CurrencyKey] ``` ### `CurrentAlertsProps` ```typescript type CurrentAlertsProps = { /** * Raw devices (with last.alerts) used to derive current alerts from. * Shape mirrors the API response from the source app. */ devices?: Device[][] isLoading?: boolean /** * Filters controlled outside (typically by URL severity param)… ``` ### `CurtailmentResult` ```typescript type CurtailmentResult = { curtailmentMWh: number curtailmentRate: number } ``` ### `DashboardDateRange` ```typescript type DashboardDateRange = { /** Window start, milliseconds since epoch. */ start: number /** Window end, milliseconds since epoch. */ end: number } ``` ### `DashboardDateRangePickerProps` ```typescript type DashboardDateRangePickerProps = { /** Current range as `{ start, end }` epoch-millisecond timestamps. */ value: DashboardDateRange /** Fires with the next `{ start, end }` window when the user applies a range. */ onChange: (next: DashboardDateRange) => void /** Display f… ``` ### `DashboardProps` ```typescript type DashboardProps = { stats?: DashboardStats isStatsLoading?: boolean alerts?: Alert[] onNavigationClick: (url: string) => void onViewAllAlerts: VoidFunction } ``` ### `DashboardStats` ```typescript type DashboardStats = { items: StatItem[] } ``` ### `DataLabelProps` ```typescript type DataLabelProps = Partial<{ /** * Range start; formatted in the active timezone (`dd/MM/yy`). */ startDate: Date | null /** * Range end; formatted in the active timezone (`dd/MM/yy`). */ endDate: Date | null /** * Label text; defaults to `PERIOD`. */ label:… ``` ### `DataTableProps` ```typescript type DataTableProps = { /** * The data to be shown in the table. See https://tanstack.com/table/v8/docs/guide/data */ data: I[] /** * The column configuration table. See https://tanstack.com/table/v8/docs/guide/column-defs */ columns: DataTableColumnDef… ``` ### `DateFormatType` ```typescript type DateFormatType = | typeof DATE_FORMAT | typeof DATE_FORMAT_SLASHED | typeof SHORT_DATE_FORMAT ``` ### `DatePickerProps` ```typescript type DatePickerProps = { /** * Currently selected date */ selected?: Date /** * Callback when date changes */ onSelect?: (date: Date | undefined) => void /** * Placeholder text when no date is selected * @default "Pick a date" */ placeholder?: string /** * Date… ``` ### `DateRange` ```typescript type DateRange = { from: Date | undefined to?: Date | undefined } ``` ### `DateRangeKey` ```typescript type DateRangeKey = keyof typeof DATE_RANGE ``` ### `DateRangePickerProps` ```typescript type DateRangePickerProps = { /** * Selected date range */ selected?: DateRange /** * Callback when date range changes */ onSelect?: (range: DateRange | undefined) => void /** * Placeholder text when no range is selected * @default "Pick a date range" */ placeholder?… ``` ### `DateRangeValue` ```typescript type DateRangeValue = (typeof DATE_RANGE)[DateRangeKey] ``` ### `DateTimeFormatType` ```typescript type DateTimeFormatType = typeof DATE_TIME_FORMAT | typeof DATE_TIME_FORMAT_WITH_SECONDS ``` ### `DetailLegendItem` ```typescript type DetailLegendItem = { /** Legend label text */ label: string /** Color for the legend icon */ color: string /** Custom icon to display (defaults to a colored square) */ icon?: ReactNode /** Current value display */ currentValue?: { value: number | string unit… ``` ### `DetailLegendProps` ```typescript type DetailLegendProps = { /** Legend items to display */ items: DetailLegendItem[] /** Callback when a legend item is toggled */ onToggle?: (label: string, index: number) => void /** Custom class name */ className?: string } ``` ### `Device` ```typescript type Device = { id: string type: string tags?: string[] rack?: string last?: DeviceLast username?: string info?: DeviceInfo containerId?: string address?: string | null code?: string alerts?: Alert[] | null powerMeters?: Device[] tempSensors?: Device[]… ``` ### `DeviceAttributeView` A single labelled device attribute row. ```typescript type DeviceAttributeView = { label: string value: string } ``` ### `DeviceData` ```typescript type DeviceData = { id: string | undefined type: string | undefined tags?: string[] rack?: string snap: ContainerSnap alerts?: Alert[] | null username?: string info?: DeviceInfo containerId?: string address?: string | null err?: string | null [key: string]:… ``` ### `DeviceExplorerDeviceData` ```typescript type DeviceExplorerDeviceData = Device ``` ### `DeviceExplorerDeviceType` ```typescript type DeviceExplorerDeviceType = (typeof DEVICE_EXPLORER_DEVICE_TYPE)[keyof typeof DEVICE_EXPLORER_DEVICE_TYPE] ``` ### `DeviceExplorerFilterOption` ```typescript type DeviceExplorerFilterOption = { value: string label: string children?: DeviceExplorerFilterOption[] } ``` ### `DeviceExplorerProps` ```typescript type DeviceExplorerProps = { deviceType: DeviceExplorerDeviceType className?: string selectedDevices?: DataTableRowSelectionState onSelectedDevicesChange?: (selections: DataTableRowSelectionState) => void } & ForwardedToolbarProps & ForwardedTableProps ``` ### `DeviceExplorerSearchOption` ```typescript type DeviceExplorerSearchOption = { value: string label: string } ``` ### `DeviceInfo` ```typescript type DeviceInfo = { container?: string pos?: string poolConfig?: string serialNum?: string macAddress?: string | null posHistory?: Partial [key: string]: unknown } ``` ### `DeviceLast` ```typescript type DeviceLast = { err?: string | null type?: string snap?: ContainerSnap alerts?: Alert[] | null [key: string]: unknown } ``` ### `DeviceStatus` ```typescript type DeviceStatus = (typeof DEVICE_STATUS)[keyof typeof DEVICE_STATUS] ``` ### `DialogContentProps` ```typescript type DialogContentProps = { title?: string description?: string closeOnClickOutside?: boolean closeOnEscape?: boolean } & DialogHeaderProps ``` ### `DialogHeaderProps` ```typescript type DialogHeaderProps = { bare?: boolean closable?: boolean onClose?: VoidFunction } ``` ### `DisplayMode` ```typescript type DisplayMode = typeof CURRENCY.USD_LABEL | typeof CURRENCY.BTC_LABEL ``` ### `DividerOrientation` ```typescript type DividerOrientation = 'horizontal' | 'vertical' ``` ### `DividerProps` ```typescript type DividerProps = { /** Line orientation */ orientation?: DividerOrientation /** Line style */ dashed?: boolean dotted?: boolean /** Text or node rendered in the middle of the divider */ children?: ReactNode /** Horizontal alignment of the label */ align?:… ``` ### `DividerType` ```typescript type DividerType = 'solid' | 'dashed' | 'dotted' ``` ### `DoughnutChartDataset` ```typescript type DoughnutChartDataset = { label: string value: number color?: string } ``` ### `DoughnutChartProps` ```typescript type DoughnutChartProps = { /** Array of labelled slices */ data: DoughnutChartDataset[] /** Unit suffix shown in tooltips */ unit?: string /** Chart.js options – merged with defaults */ options?: ChartJS<'doughnut'>['options'] /** Doughnut cutout percentage (defau… ``` ### `EbitdaChartsProps` ```typescript type EbitdaChartsProps = { showEbitdaBarChart: boolean ebitdaChartData: BarChartDataResult btcDisplayData: BarChartDataResult isLoading: boolean hasBtcProducedAllZeros: boolean } ``` ### `EbitdaDisplayMetrics` Display metrics for the financial EBITDA reporting surface (foundation). Values mirror Mining OS `Views/Financial/EBITDA/EBITDA.types` `EbitdaMetrics`. ```typescript type EbitdaDisplayMetrics = { bitcoinProductionCost: number bitcoinPrice: number bitcoinProduced: number ebitdaSellingBTC: number actualEbitda: number ebitdaNotSellingBTC: number } ``` ### `EbitdaHodlCardProps` ```typescript type EbitdaHodlCardProps = { value: number currentBTCPrice: number } ``` ### `EbitdaLogEntry` ```typescript type EbitdaLogEntry = { ts: number revenueBTC: number revenueUSD: number btcPrice: number powerW: number hashrateMhs: number consumptionMWh: number energyCostsUSD: number operationalCostsUSD: number totalCostsUSD: number ebitdaSelling: number ebitdaHodl: number… ``` ### `EbitdaMetricsProps` ```typescript type EbitdaMetricsProps = { metrics: EbitdaDisplayMetrics currentBTCPrice: number } ``` ### `EbitdaProps` ```typescript type EbitdaProps = { metrics: EbitdaDisplayMetrics | null ebitdaChartInput: ToBarChartDataInput | null btcProducedChartInput: ToBarChartDataInput | null hasBtcProducedAllZeros: boolean showEbitdaBarChart: boolean currentBTCPrice: number datePicker: ReactElem… ``` ### `EbitdaQueryParams` ```typescript type EbitdaQueryParams = { start: number end: number period: FinancePeriod } ``` ### `EbitdaResponse` ```typescript type EbitdaResponse = FinanceResponse ``` ### `EbitdaSellingCardProps` ```typescript type EbitdaSellingCardProps = { value: number } ``` ### `EbitdaTotals` ```typescript type EbitdaTotals = { totalRevenueBTC: number totalRevenueUSD: number totalCostsUSD: number totalEbitdaSelling: number totalEbitdaHodl: number avgBtcProductionCost: number | null currentBtcPrice: number } ``` ### `EbitdaViewModel` ```typescript type EbitdaViewModel = { metrics: EbitdaDisplayMetrics | null ebitdaChartInput: ToBarChartDataInput | null btcProducedChartInput: ToBarChartDataInput | null hasBtcProducedAllZeros: boolean currentBTCPrice: number /** When false, hide the EBITDA bar group (Mining… ``` ### `EfficiencyDateRange` ```typescript type EfficiencyDateRange = { start: number; end: number } ``` ### `EfficiencyMinerTypeTailLog` ```typescript type EfficiencyMinerTypeTailLog = Record ``` ### `EfficiencyMinerTypeViewProps` ```typescript type EfficiencyMinerTypeViewProps = Omit ``` ### `EfficiencyMinerUnitTailLog` ```typescript type EfficiencyMinerUnitTailLog = Record ``` ### `EfficiencyMinerUnitViewProps` ```typescript type EfficiencyMinerUnitViewProps = Omit ``` ### `EfficiencySiteViewProps` ```typescript type EfficiencySiteViewProps = { log?: MetricsEfficiencyLogEntry[] avgEfficiency?: number | null nominalValue?: number | null isLoading?: boolean dateRange?: EfficiencyDateRange onDateRangeChange?: (range: EfficiencyDateRange) => void onReset?: VoidFunction } ``` ### `EfficiencyTabValue` ```typescript type EfficiencyTabValue = (typeof EfficiencyTabTypes)[keyof typeof EfficiencyTabTypes] ``` ### `EmptyStateImage` ```typescript type EmptyStateImage = 'default' | 'simple' | ReactNode ``` ### `EmptyStateProps` ```typescript type EmptyStateProps = { /** * Description text or ReactNode displayed below the image */ description: ReactNode /** * Image to display. Use "default" for the standard illustration, * "simple" for a minimal icon, or pass a custom ReactNode. * @default "default"… ``` ### `EmptyStateSize` ```typescript type EmptyStateSize = ComponentSize ``` ### `EnabledDisableToggleProps` ```typescript type EnabledDisableToggleProps = { value: unknown tankNumber: number | string isButtonDisabled: boolean isOffline: boolean onToggle: (params: EnabledDisableToggleCbParams) => void } ``` ### `EnergyBalanceCostChartsProps` ```typescript type EnergyBalanceCostChartsProps = { costChartData: BarChartDataResult btcUnit: EnergyCostChartInput['btcUnit'] powerChartInput: ThresholdLineChartInput displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => void /** Sh… ``` ### `EnergyBalanceCostMetricsProps` ```typescript type EnergyBalanceCostMetricsProps = { metrics: EnergyCostMetrics } ``` ### `EnergyBalanceData` ```typescript type EnergyBalanceData = { revenueMetrics: EnergyRevenueMetrics | null costMetrics: EnergyCostMetrics | null energyRevenueChartInput: ThresholdBarChartInput averageDowntimeData: AverageDowntimeChartData powerChartInput: ThresholdLineChartInput powerChartCostInput:… ``` ### `EnergyBalanceLogEntry` ```typescript type EnergyBalanceLogEntry = { ts: number powerW: number consumptionMWh: number revenueBTC: number revenueUSD: number btcPrice: number energyCostUSD: number totalCostUSD: number energyRevenuePerMWh: number | null allInCostPerMWh: number | null profitUSD: number curtai… ``` ### `EnergyBalancePowerChartProps` ```typescript type EnergyBalancePowerChartProps = { height?: number fillHeight?: boolean periodType: PeriodType chartInput: ThresholdLineChartInput } ``` ### `EnergyBalanceProps` ```typescript type EnergyBalanceProps = { viewModel: EnergyBalanceViewModel onTabChange: (tab: EnergyBalanceTab) => void onRevenueDisplayModeChange: (mode: DisplayMode) => void onCostDisplayModeChange: (mode: DisplayMode) => void isDemoMode?: boolean /** Slot for timeframe / dat… ``` ### `EnergyBalanceQueryParams` ```typescript type EnergyBalanceQueryParams = { start: number end: number period: FinancePeriod } ``` ### `EnergyBalanceResponse` ```typescript type EnergyBalanceResponse = FinanceResponse ``` ### `EnergyBalanceRevenueChartsProps` ```typescript type EnergyBalanceRevenueChartsProps = { revenueChartData: BarChartDataResult averageDowntimeData: AverageDowntimeChartData powerChartInput: ThresholdLineChartInput displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => voi… ``` ### `EnergyBalanceRevenueMetricsProps` ```typescript type EnergyBalanceRevenueMetricsProps = { metrics: EnergyRevenueMetrics } ``` ### `EnergyBalanceTab` ```typescript type EnergyBalanceTab = 'revenue' | 'cost' ``` ### `EnergyBalanceTotals` ```typescript type EnergyBalanceTotals = { totalRevenueBTC: number totalRevenueUSD: number totalCostUSD: number totalProfitUSD: number avgCostPerMWh: number | null avgRevenuePerMWh: number | null totalConsumptionMWh: number avgCurtailmentRate: number | null avgOperationalIssuesRa… ``` ### `EnergyBalanceViewModel` ```typescript type EnergyBalanceViewModel = EnergyBalanceData & { activeTab: EnergyBalanceTab revenueDisplayMode: DisplayMode costDisplayMode: DisplayMode isLoading: boolean errors: string[] hasDateSelection: boolean } ``` ### `EnergyCostChartInput` ```typescript type EnergyCostChartInput = ThresholdBarChartInput & { btcUnit: string | null } ``` ### `EnergyCostChartProps` ```typescript type EnergyCostChartProps = { chartData: BarChartDataResult btcUnit: EnergyCostChartInput['btcUnit'] displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => void height?: number } ``` ### `EnergyCostMetrics` ```typescript type EnergyCostMetrics = { avgPowerConsumption: number avgEnergyCost: number avgAllInCost: number avgPowerAvailability: number avgOperationsCost: number avgEnergyRevenue: number } ``` ### `EnergyMetricCardProps` ```typescript type EnergyMetricCardProps = { name: string value: number unit: string fallback?: string } ``` ### `EnergyReportBarChartLegacy` ```typescript type EnergyReportBarChartLegacy = { labels: string[] dataSet1: { label: string; data: number[] } } ``` ### `EnergyReportBarViewProps` ```typescript type EnergyReportBarViewProps = { title: string isEmpty?: boolean isLoading?: boolean chartInput?: ToBarChartDataInput onTimeFrameChange?: (start: Date, end: Date) => void } ``` ### `EnergyReportContainer` ```typescript type EnergyReportContainer = Container & { containerId?: string minersCount?: number } ``` ### `EnergyReportDateRange` ```typescript type EnergyReportDateRange = { start: number; end: number } ``` ### `EnergyReportGroupedBarViewProps` ```typescript type EnergyReportGroupedBarViewProps = { isLoading?: boolean containers?: Container[] groupedConsumption?: MetricsConsumptionGroupedResponse onTimeFrameChange?: EnergyReportBarViewProps['onTimeFrameChange'] } ``` ### `EnergyReportMinerSliceConfig` ```typescript type EnergyReportMinerSliceConfig = { title: string groupBy: MetricsConsumptionGroupBy filterCategory?: (category: string) => boolean getLabelName: (category: string, containers?: Container[]) => string } ``` ### `EnergyReportMinerTypeViewProps` ```typescript type EnergyReportMinerTypeViewProps = EnergyReportGroupedBarViewProps ``` ### `EnergyReportMinerUnitViewProps` ```typescript type EnergyReportMinerUnitViewProps = EnergyReportGroupedBarViewProps ``` ### `EnergyReportMinerViewSlice` ```typescript type EnergyReportMinerViewSlice = (typeof ENERGY_REPORT_MINER_VIEW_SLICES)[keyof typeof ENERGY_REPORT_MINER_VIEW_SLICES] ``` ### `EnergyReportProps` ```typescript type EnergyReportProps = { defaultTab?: EnergyReportTabValue siteView?: Omit & { dateRange?: EnergyReportDateRange } minerTypeView?: EnergyReportMinerTypeViewProps minerUnitView?: EnergyReportMinerUnitViewProps className?: s… ``` ### `EnergyReportSiteViewProps` ```typescript type EnergyReportSiteViewProps = Omit & { snapshotLoading?: boolean onRefetchSnapshot?: VoidFunction dateRange: EnergyReportDateRange onDateRangeChange?: (range: EnergyReportDateRange) => void } ``` ### `EnergyReportTabValue` ```typescript type EnergyReportTabValue = (typeof ENERGY_REPORT_TAB_TYPES)[keyof typeof ENERGY_REPORT_TAB_TYPES] ``` ### `EnergyReportTailLogItem` ```typescript type EnergyReportTailLogItem = Record< string, EnergyReportTailLogNumericBucket | number | undefined > ``` ### `EnergyReportTailLogNumericBucket` ```typescript type EnergyReportTailLogNumericBucket = Record ``` ### `EnergyRevenueChartProps` ```typescript type EnergyRevenueChartProps = { chartData: BarChartDataResult displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => void height?: number } ``` ### `EnergyRevenueMetrics` ```typescript type EnergyRevenueMetrics = { curtailmentRate: number operationalIssuesRate: number } ``` ### `EntryData` ```typescript type EntryData = { [key: string]: number | null | undefined } ``` ### `ErrorBoundaryProps` ```typescript type ErrorBoundaryProps = { /** * Fallback UI to render when an error is caught. * If not provided, a default error panel with expandable stack trace is shown. */ fallback?: ReactNode /** * Name of the wrapped component (shown in the default fallback) */ componentN… ``` ### `ErrorCardProps` ```typescript type ErrorCardProps = { /** * Error message string. Supports `\n` for line breaks. */ error: string /** * Title displayed above the error message * @default "Errors" */ title?: string /** * Display variant. "card" shows a bordered container, "inline" shows flat… ``` ### `ErrorCardVariant` ```typescript type ErrorCardVariant = 'card' | 'inline' ``` ### `ErrorWithTimestamp` ```typescript type ErrorWithTimestamp = { msg?: string message?: string timestamp?: number | string } ``` ### `ExplorerDetailProps` ```typescript type ExplorerDetailProps = { /** The active Explorer tab — selects which per-type panel renders. */ deviceType: DeviceExplorerDeviceType /** Router navigate used by alarm rows to deep-link into `/alerts/:id`. */ onNavigate?: (path: string) => void /** Compact layout… ``` ### `ExplorerLayoutProps` ```typescript type ExplorerLayoutProps = { /** Page heading. */ title?: string /** Optional header controls (export button, etc.) shown next to the title. */ headerActions?: ReactNode /** The list column — typically a tab switch plus the device/container table. */ list: ReactNode… ``` ### `ExplorerRowSelection` Row-selection map from `<DeviceExplorer>` — thing id → selected. ```typescript type ExplorerRowSelection = Record ``` ### `ExplorerThingDetailRow` ```typescript type ExplorerThingDetailRow = { label: string value: string } ``` ### `ExportButtonProps` ```typescript type ExportButtonProps = { /** Fires with the chosen format when the user picks an item. */ onExport: (format: ExportFormat) => void /** Formats to offer in the dropdown — defaults to `['csv', 'json']`. */ formats?: readonly ExportFormat[] /** Button label — defau… ``` ### `ExportFormat` ```typescript type ExportFormat = 'csv' | 'json' ``` ### `ExtraTooltipData` ```typescript type ExtraTooltipData = Record ``` ### `FeatureFlagsSettingsProps` ```typescript type FeatureFlagsSettingsProps = { featureFlags: Record isEditingEnabled: boolean isLoading?: boolean isSaving?: boolean onSave: (flags: Record) => void className?: string } ``` ### `FieldName` Extract field names from a form schema with type safety. Useful for creating type-safe field name constants. ```typescript type FieldName = FieldPath ``` ### `FilterSelectionTuple` ```typescript type FilterSelectionTuple = CascaderValue ``` ### `FinancePeriod` Finance v2 API types - shapes returned by `/auth/finance/*` endpoints. These are the contracts consumers feed back through the reporting-tool base hooks (`useEbitda`, `useEnergyBalance`, ...). The mdk hooks accept `{ data, isLoading, error }` where `data` matches the *Response types declared here. Foundation does not own the RTK Query / HTTP layer itself - see api-integration-todo.md. ```typescript type FinancePeriod = 'daily' | 'weekly' | 'monthly' | 'yearly' ``` ### `FinanceQueryParams` ```typescript type FinanceQueryParams = { start: number end: number period?: FinancePeriod overwriteCache?: boolean } ``` ### `FinanceResponse` ```typescript type FinanceResponse = { log: Log[] summary: Summary } ``` ### `FinanceRevenueLogEntry` ```typescript type FinanceRevenueLogEntry = { ts: number revenueBTC: number feesBTC: number netRevenueBTC: number } ``` ### `FinanceRevenueQueryParams` ```typescript type FinanceRevenueQueryParams = FinanceQueryParams & { pool?: string } ``` ### `FinanceRevenueResponse` ```typescript type FinanceRevenueResponse = FinanceResponse ``` ### `FinanceRevenueTotals` ```typescript type FinanceRevenueTotals = { totalRevenueBTC: number totalFeesBTC: number totalNetRevenueBTC: number } ``` ### `FinancialDateRange` ```typescript type FinancialDateRange = { start: number end: number period?: PeriodValue } ``` ### `FinancialRangeChangeOptions` ```typescript type FinancialRangeChangeOptions = { year?: number month?: number period?: PeriodValue } ``` ### `FlexAlign` Flex/Grid alignment options Used by Popover, charts, and layout components ```typescript type FlexAlign = 'start' | 'center' | 'end' ``` ### `FormatHashBalanceValueOptions` ```typescript type FormatHashBalanceValueOptions = { forAxis?: boolean } ``` ### `FormCascaderProps` ```typescript type FormCascaderProps = BaseFormFieldProps & { options: CascaderOption[] multiple?: boolean cascaderProps?: Omit< React.ComponentProps, 'value' | 'onChange' | 'options' | 'placeholder' > } ``` ### `FormCheckboxProps` ```typescript type FormCheckboxProps = BaseFormFieldProps & { checkboxProps?: React.ComponentProps layout?: 'row' | 'column' } ``` ### `FormDatePickerProps` ```typescript type FormDatePickerProps = BaseFormFieldProps & { datePickerProps?: Omit, 'selected' | 'onSelect'> } ``` ### `FormInputProps` ```typescript type FormInputProps = BaseFormFieldProps & { type?: React.ComponentProps['type'] variant?: React.ComponentProps['variant'] inputProps?: Omit, 'type' | 'variant'> } ``` ### `FormProps` Form wrapper that provides react-hook-form context to child components. ```typescript type FormProps = Omit< ComponentProps<'form'>, 'children' > & { form: UseFormReturn children: ReactNode } ``` ### `FormRadioGroupProps` ```typescript type FormRadioGroupProps = BaseFormFieldProps & { options: FormRadioOption[] orientation?: 'horizontal' | 'vertical' radioGroupProps?: Omit< React.ComponentProps, 'onValueChange' | 'defaultValue' | 'orientation' > } ``` ### `FormRadioOption` ```typescript type FormRadioOption = { value: string label: string disabled?: boolean } ``` ### `FormSelectOption` ```typescript type FormSelectOption = { value: string label: string disabled?: boolean } ``` ### `FormSelectProps` ```typescript type FormSelectProps = BaseFormFieldProps & { options: FormSelectOption[] selectProps?: Omit, 'onValueChange' | 'defaultValue'> } ``` ### `FormSwitchProps` ```typescript type FormSwitchProps = BaseFormFieldProps & { switchProps?: Omit, 'checked' | 'onCheckedChange'> layout?: 'row' | 'column' } ``` ### `FormTagInputProps` ```typescript type FormTagInputProps = BaseFormFieldProps & { options?: TagInputOption[] allowCustomTags?: boolean variant?: 'default' | 'search' tagInputProps?: Omit< React.ComponentProps, 'value' | 'onTagsChange' | 'label' | 'placeholder'… ``` ### `FormTextAreaProps` ```typescript type FormTextAreaProps = BaseFormFieldProps & { textAreaProps?: React.ComponentProps } ``` ### `GaugeChartProps` ```typescript type GaugeChartProps = { /** Value between 0 and 1 (e.g. 0.75 = 75%). Values outside the range are clamped. */ percent: number /** Arc colours in HEX format. */ colors?: string[] /** Arc thickness as a fraction of the gauge radius (0–1). */ arcWidth?: number /**… ``` ### `GetAlertsTableColumnsOptions` ```typescript type GetAlertsTableColumnsOptions = { getFormattedDate: (date: Date) => string } ``` ### `GetColumnConfigParams` ```typescript type GetColumnConfigParams = { getFormattedDate: (date: Date) => string renderAction: (device: DeviceExplorerDeviceData) => React.ReactNode } ``` ### `GlobalConfig` ```typescript type GlobalConfig = { nominalSiteHashrate_MHS?: number nominalAvailablePowerMWh?: number nominalPowerConsumption_MW?: number nominalWeightedAvgEfficiency_WThs?: number nominalMinerCapacity?: number isAutoSleepAllowed?: boolean siteEnergyDataThresholdMWh?: num… ``` ### `HashBalanceCostPanelProps` ```typescript type HashBalanceCostPanelProps = HashBalancePanelProps ``` ### `HashBalanceCurrency` ```typescript type HashBalanceCurrency = typeof CURRENCY.USD_LABEL | typeof CURRENCY.BTC_LABEL ``` ### `HashBalanceMetric` ```typescript type HashBalanceMetric = { label: string unit: string value: number isHighlighted?: boolean } ``` ### `HashBalancePanelProps` ```typescript type HashBalancePanelProps = Pick< UseHashBalanceInput, 'data' | 'log' | 'dateRange' | 'timeframeType' > & { isLoading?: boolean } ``` ### `HashBalanceProps` ```typescript type HashBalanceProps = Partial<{ isError: boolean isLoading: boolean errorMessage: string className: string tabsClassName: string tabsListClassName: string data: HashRevenueResponse | null initialDateRange: FinancialDateRange onDateRangeChange: (dateRange: Finan… ``` ### `HashBalanceRevenuePanelProps` ```typescript type HashBalanceRevenuePanelProps = HashBalancePanelProps & { currency: HashBalanceCurrency onCurrencyChange: (currency: HashBalanceCurrency) => void } ``` ### `HashrateBarChartData` ```typescript type HashrateBarChartData = { labels: string[] series: { label: string values: number[] color: string }[] } ``` ### `HashrateDateRange` ```typescript type HashrateDateRange = { start: number; end: number } ``` ### `HashrateFilterOption` ```typescript type HashrateFilterOption = { value: string label: string } ``` ### `HashrateGroupBy` groupBy axis for the v2 grouped hashrate endpoint. - `miner` returns one bucket per miner-type id. - `container` returns one bucket per container id (plus rollup keys that the utils layer filters out - see `getCleanGroupedEntries`). ```typescript type HashrateGroupBy = 'miner' | 'container' ``` ### `HashrateGroupedLog` ```typescript type HashrateGroupedLog = ReadonlyArray ``` ### `HashrateGroupedLogEntry` Single bucket of a v2 `/auth/metrics/hashrate` grouped response. `hashrateMhs` keys are either miner-type ids (`groupBy=miner`) or container ids (`groupBy=container`) depending on which response the consumer fetched. Values are in MH/s; the view layer converts to TH/s for display. ```typescript type HashrateGroupedLogEntry = { ts: number hashrateMhs: Record } ``` ### `HashrateGroupedQueryData` ```typescript type HashrateGroupedQueryData = { log?: HashrateGroupedLog } ``` ### `HashrateLabelDivisorKey` ```typescript type HashrateLabelDivisorKey = keyof typeof HASHRATE_LABEL_DIVISOR ``` ### `HashrateLabelDivisorValue` ```typescript type HashrateLabelDivisorValue = (typeof HASHRATE_LABEL_DIVISOR)[HashrateLabelDivisorKey] ``` ### `HashrateMinerTypeViewProps` ```typescript type HashrateMinerTypeViewProps = { /** Hashrate log grouped by miner type (groupBy=miner). */ log?: HashrateGroupedLog isLoading?: boolean dateRange?: HashrateDateRange onDateRangeChange?: (range: HashrateDateRange) => void onReset?: VoidFunction } ``` ### `HashrateMiningUnitViewProps` ```typescript type HashrateMiningUnitViewProps = { /** Hashrate log grouped by container / mining unit (groupBy=container). */ log?: HashrateGroupedLog isLoading?: boolean dateRange?: HashrateDateRange onDateRangeChange?: (range: HashrateDateRange) => void onReset?: VoidFunction } ``` ### `HashrateProps` ```typescript type HashrateProps = { /** Tab selected on first render. Defaults to the Site View. */ defaultTab?: HashrateTabValue /** Props forwarded to the Site View tab. */ siteView?: HashrateSiteViewProps /** Props forwarded to the Miner Type View tab. */ minerTypeView?… ``` ### `HashrateQueryState` ```typescript type HashrateQueryState = { data?: HashrateGroupedQueryData isLoading?: boolean error?: unknown } ``` ### `HashrateSeries` ```typescript type HashrateSeries = { label: string color?: string points: HashrateSeriesPoint[] } ``` ### `HashrateSeriesPoint` ```typescript type HashrateSeriesPoint = { ts: string value: number } ``` ### `HashrateSiteViewChartData` ```typescript type HashrateSiteViewChartData = { series: HashrateSeries[] } ``` ### `HashrateSiteViewProps` ```typescript type HashrateSiteViewProps = { /** Hashrate log grouped by miner type. */ log?: HashrateGroupedLog /** Loading state - drives the chart spinner. */ isLoading?: boolean /** Selected date range used by the host to drive the query. */ dateRange?: HashrateDateRange /** Fi… ``` ### `HashrateTabValue` ```typescript type HashrateTabValue = (typeof HashrateTabTypes)[keyof typeof HashrateTabTypes] ``` ### `HashrateUnit` ```typescript type HashrateUnit = ValueUnit & { unit: string } ``` ### `HashRevenueLogEntry` ```typescript type HashRevenueLogEntry = { ts: number revenueBTC: number feesBTC: number revenueUSD: number feesUSD: number btcPrice: number hashrateMhs: number hashRevenueBTCPerPHsPerDay: number | null hashRevenueUSDPerPHsPerDay: number | null hashCostBTCPerPHsPerDay: number | n… ``` ### `HashRevenueResponse` ```typescript type HashRevenueResponse = FinanceResponse ``` ### `HashRevenueTotals` ```typescript type HashRevenueTotals = { avgHashRevenueBTCPerPHsPerDay: number | null avgHashRevenueUSDPerPHsPerDay: number | null avgHashCostBTCPerPHsPerDay: number | null avgHashCostUSDPerPHsPerDay: number | null avgNetworkHashPriceBTCPerPHsPerDay: number | null avgNetworkHas… ``` ### `HeaderConsumptionBoxProps` ```typescript type HeaderConsumptionBoxProps = { icon?: ReactNode /** Current site-level power consumption, in megawatts. */ valueMw?: number /** Unit label — defaults to `MW`. */ unit?: string className?: string } ``` ### `HeaderControlsSettingsProps` ```typescript type HeaderControlsSettingsProps = { preferences: HeaderPreferences isLoading?: boolean onToggle: (key: keyof HeaderPreferences, value: boolean) => void onReset: VoidFunction className?: string } ``` ### `HeaderEfficiencyBoxProps` ```typescript type HeaderEfficiencyBoxProps = { icon?: ReactNode /** Efficiency in watts per TH/s. */ valueWthS?: number /** Unit label — defaults to `W/TH/S`. */ unit?: string className?: string } ``` ### `HeaderHashrateBoxProps` ```typescript type HeaderHashrateBoxProps = { icon?: ReactNode /** MOS-side aggregate hashrate in PH/s. */ mosPhs?: number /** Pool-side aggregate hashrate in PH/s. */ poolPhs?: number /** Hashrate unit label — defaults to `PH/s`. */ unit?: string className?: string } ``` ### `HeaderMinersBoxProps` ```typescript type HeaderMinersBoxProps = { /** Icon shown next to the "Miners" label. Caller-provided so the package stays icon-agnostic. */ icon?: ReactNode /** Total miners across the site (denominator of the `158 / 2,188` ratio). */ total?: number /** Online miners (the `158`… ``` ### `HeaderPreferences` ```typescript type HeaderPreferences = { poolMiners: boolean mosMiners: boolean poolHashrate: boolean mosHashrate: boolean consumption: boolean efficiency: boolean } ``` ### `HeaderStatsBarProps` ```typescript type HeaderStatsBarProps = { /** Stat boxes to render in order, left-to-right. */ children: ReactNode /** Optional class hook. */ className?: string } ``` ### `HeatmapCell` A single heatmap cell. ```typescript type HeatmapCell = { /** Numeric value driving the cell colour; `null` renders the empty colour. */ value: number | null /** Text shown in the cell (defaults to the value when `showValues` is on). */ label?: string /** Stable React key (defaults to the row/c… ``` ### `HeatmapCellContext` Context handed to `renderCell` for a single cell. ```typescript type HeatmapCellContext = { /** The colour resolved for this cell from the gradient (or the empty colour). */ color: string /** Zero-based row index. */ row: number /** Zero-based column index. */ col: number } ``` ### `HeatmapKey` ```typescript type HeatmapKey = keyof typeof HEATMAP ``` ### `HeatmapLegendProps` ```typescript type HeatmapLegendProps = { /** Value (or pre-formatted label) at the low end of the scale. */ min: number | string /** Value (or pre-formatted label) at the high end of the scale. */ max: number | string /** Unit suffix appended to `min`/`max`. */ unit?: string /*… ``` ### `HeatmapModeKey` ```typescript type HeatmapModeKey = keyof typeof HEATMAP_MODE ``` ### `HeatmapModeValue` ```typescript type HeatmapModeValue = (typeof HEATMAP_MODE)[HeatmapModeKey] ``` ### `HeatmapProps` ```typescript type HeatmapProps = { /** Rows of cells (row-major). Rows may be ragged. */ data: HeatmapCell[][] /** Range floor; auto-derived from the finite values when omitted. */ min?: number /** Range ceiling; auto-derived from the finite values when omitted. */ max?:… ``` ### `HighlightedValueProps` ```typescript type HighlightedValueProps = { value: string | number unit?: string className?: string style?: CSSProperties } ``` ### `HistoricalAlertsProps` ```typescript type HistoricalAlertsProps = { /** * Pre-fetched historical alerts log entries (each with a `thing` device payload). */ alerts?: Alert[] isLoading?: boolean /** * Filters and search tags coming from the parent (typically shared with `CurrentAlerts`). */ localFilters:… ``` ### `HistoricalAlertsRange` ```typescript type HistoricalAlertsRange = { start: number end: number } ``` ### `IconProps` ```typescript type IconProps = { /** Sets both width and height */ size?: number | string /** Only affects single-color icons (default: 'currentColor') */ color?: string children?: never } & SVGAttributes ``` ### `ImportExportSettingsProps` ```typescript type ImportExportSettingsProps = { onExport: VoidFunction onImport: (data: SettingsExportData) => void onParseFile?: (file: File) => Promise isExporting?: boolean isImporting?: boolean className?: string } ``` ### `IndicatorColor` ```typescript type IndicatorColor = | 'red' | 'gray' | 'blue' | 'yellow' | 'green' | 'purple' | 'amber' | 'slate' ``` ### `IndicatorProps` ```typescript type IndicatorProps = { /** * Color variant of the indicator * @default 'gray' */ color?: IndicatorColor /** * Size variant of the indicator * @default 'md' */ size?: ComponentSize /** * Custom className for the root element */ className?: string /** * When tru… ``` ### `InputProps` ```typescript type InputProps = Omit, 'prefix' | 'size'> & { /** * Optional label displayed above the input */ label?: string /** * HTML id for the input. Required when using label for accessibility. */ id?: string /** * Variant of the input * - `… ``` ### `InputSize` ```typescript type InputSize = 'default' | 'medium' ``` ### `LabeledCardNavigateOptions` ```typescript type LabeledCardNavigateOptions = Partial<{ href: string target: string }> ``` ### `LabeledCardProps` ```typescript type LabeledCardProps = Partial<{ isDark: boolean className: string hasNoWrap: boolean isRelative: boolean isFullWidth: boolean hasNoMargin: boolean hasNoBorder: boolean isFullHeight: boolean isScrollable: boolean label: ReactNode children: ReactNode getNavigateO… ``` ### `LabelToIgnoreValue` ```typescript type LabelToIgnoreValue = (typeof LABEL_TO_IGNORE)[number] ``` ### `LegendItem` ```typescript type LegendItem = { label: string color: string hidden?: boolean } ``` ### `LightWeightLineChartProps` ```typescript type LightWeightLineChartProps = { /** * Mutable ref to hold the LightWeightCharts reference */ chartRef?: MutableRefObject /** * Data of the chart */ data: LineChartData /** * Callback to format ticks on y axis. If `priceFormatter` is given. It would be… ``` ### `LineChartCardData` ```typescript type LineChartCardData = { /** Chart datasets */ datasets: LineChartCardDataset[] } & Partial<{ /** Y-axis tick formatter */ yTicksFormatter: (value: number) => string /** Price formatter for lightweight-charts */ priceFormatter: (value: number) => string /** Whet… ``` ### `LineChartCardDataset` ```typescript type LineChartCardDataset = { /** Label for the dataset (used in legend) */ label?: string /** Line color */ borderColor: string /** Data points */ data: Array<{ x: number; y: number | null }> /** Whether this dataset is visible */ visible?: boolean /** Custom icon f… ``` ### `LineChartCardProps` ```typescript type LineChartCardProps = Partial<{ /** Pre-adapted chart data (use this OR rawData+dataAdapter) */ data: LineChartCardData /** Raw data to be transformed by dataAdapter */ rawData: unknown /** Adapter to transform rawData into LineChartCardData */ dataAdapter: (da… ``` ### `LineChartData` ```typescript type LineChartData = { datasets: LineDataset[] } ``` ### `LineDataPoint` ```typescript type LineDataPoint = { x: number; y: number | null | undefined } ``` ### `LineDataset` ```typescript type LineDataset = { label?: string visible?: boolean borderColor: string borderWidth?: number extraTooltipData?: ExtraTooltipData data: LineDataPoint[] } ``` ### `LineSeriesApi` ```typescript type LineSeriesApi = ISeriesApi<'Line', Time> ``` ### `LoaderProps` ```typescript type LoaderProps = { /** * Size of each dot in pixels * @default 10 */ size?: number /** * Number of dots to display * @default 5 */ count?: 3 | 5 | 7 /** * Color variant of the loader * @default 'orange' */ color?: 'red' | 'gray' | 'blue' | 'amber' | 'orang… ``` ### `LocalFilters` ```typescript type LocalFilters = Record ``` ### `LogActivityIconProps` ```typescript type LogActivityIconProps = { status: string } ``` ### `LogData` ```typescript type LogData = { uuid: string body: string title: string status: string subtitle: string } ``` ### `LogDotProps` ```typescript type LogDotProps = { type: string status: string } ``` ### `LogFormattedAlertData` ```typescript type LogFormattedAlertData = { title: string subtitle: string status: string severityLevel: number creationDate: number | string body: string id: string uuid?: string [key: string]: unknown } ``` ### `LogItemProps` ```typescript type LogItemProps = { data: LogData onLogClicked?: (uuid: string) => void } ``` ### `LogPagination` ```typescript type LogPagination = { current: number total: number pageSize: number handlePaginationChange: (page: number) => void } ``` ### `LogRowProps` ```typescript type LogRowProps = { log: LogData type: string style?: CSSProperties onLogClicked?: (uuid: string) => void } ``` ### `LogsCardProps` ```typescript type LogsCardProps = Partial<{ type: string label: string isDark: boolean isLoading: boolean logsData: LogData[] emptyMessage: string skeletonRows: number pagination: LogPagination onLogClicked: (uuid: string) => void }> ``` ### `LvCabinetRecord` ```typescript type LvCabinetRecord = { id: string powerMeters?: PowerMeter[] } ``` ### `ManageUserModalProps` ```typescript type ManageUserModalProps = { open: boolean onClose: VoidFunction user: SettingsUser roles: RoleOption[] rolePermissions: Record> permissionLabels: Record onSubmit: (data: { id: string; name: string; email: string; ro… ``` ### `MaxUnitValueKey` ```typescript type MaxUnitValueKey = keyof typeof MAX_UNIT_VALUE ``` ### `MaxUnitValueValue` ```typescript type MaxUnitValueValue = (typeof MAX_UNIT_VALUE)[MaxUnitValueKey] ``` ### `Maybe` ```typescript type Maybe = T | null | undefined ``` ### `MdkWordmarkProps` ```typescript type MdkWordmarkProps = { /** Visual size of the wordmark. `sm` ≈ 24px tall, `md` ≈ 32px, `lg` ≈ 64px. */ size?: MdkWordmarkSize /** Optional class hook on the outer ``. */ className?: string /** Accessible label. Defaults to "MDK". */ title?: string } ``` ### `MdkWordmarkSize` ```typescript type MdkWordmarkSize = 'sm' | 'md' | 'lg' ``` ### `MetricsConsumptionGroupBy` ```typescript type MetricsConsumptionGroupBy = 'container' | 'miner' ``` ### `MetricsConsumptionGroupedLogEntry` ```typescript type MetricsConsumptionGroupedLogEntry = { ts: number powerW: Record consumptionMWh: Record | null } ``` ### `MetricsConsumptionGroupedResponse` ```typescript type MetricsConsumptionGroupedResponse = MetricsResponse< MetricsConsumptionGroupedLogEntry, MetricsConsumptionGroupedSummary > ``` ### `MetricsConsumptionGroupedSummary` ```typescript type MetricsConsumptionGroupedSummary = { avgPowerW: number | null totalConsumptionMWh: number groupedBy?: Record } ``` ### `MetricsConsumptionGroupSummary` ```typescript type MetricsConsumptionGroupSummary = { avgPowerW: number | null totalConsumptionMWh: number } ``` ### `MetricsConsumptionLogEntry` ```typescript type MetricsConsumptionLogEntry = { ts: number powerW: number consumptionMWh: number } ``` ### `MetricsConsumptionQueryParams` ```typescript type MetricsConsumptionQueryParams = MetricsQueryParams & { groupBy?: MetricsConsumptionGroupBy } ``` ### `MetricsConsumptionResponse` ```typescript type MetricsConsumptionResponse = MetricsResponse< MetricsConsumptionLogEntry, MetricsConsumptionSummary > ``` ### `MetricsConsumptionSummary` ```typescript type MetricsConsumptionSummary = { avgPowerW: number | null totalConsumptionMWh: number } ``` ### `MetricsEfficiencyLogEntry` ```typescript type MetricsEfficiencyLogEntry = { ts: number efficiencyWThs: number } ``` ### `MetricsEfficiencyResponse` ```typescript type MetricsEfficiencyResponse = MetricsResponse< MetricsEfficiencyLogEntry, MetricsEfficiencySummary > ``` ### `MetricsEfficiencySummary` ```typescript type MetricsEfficiencySummary = { avgEfficiencyWThs: number | null } ``` ### `MetricsHashrateGroupBy` ```typescript type MetricsHashrateGroupBy = 'container' | 'miner' ``` ### `MetricsHashrateGroupedLogEntry` ```typescript type MetricsHashrateGroupedLogEntry = { ts: number hashrateMhs: Record } ``` ### `MetricsHashrateGroupedResponse` ```typescript type MetricsHashrateGroupedResponse = MetricsResponse< MetricsHashrateGroupedLogEntry, MetricsHashrateGroupedSummary > ``` ### `MetricsHashrateGroupedSummary` ```typescript type MetricsHashrateGroupedSummary = { avgHashrateMhs: number | null totalHashrateMhs: number groupedBy?: Record } ``` ### `MetricsHashrateLogEntry` ```typescript type MetricsHashrateLogEntry = { ts: number hashrateMhs: number } ``` ### `MetricsHashrateQueryParams` ```typescript type MetricsHashrateQueryParams = MetricsQueryParams & { groupBy?: MetricsHashrateGroupBy } ``` ### `MetricsHashrateResponse` ```typescript type MetricsHashrateResponse = MetricsResponse< MetricsHashrateLogEntry, MetricsHashrateSummary > ``` ### `MetricsHashrateSummary` ```typescript type MetricsHashrateSummary = { avgHashrateMhs: number | null totalHashrateMhs: number } ``` ### `MetricsInterval` ```typescript type MetricsInterval = '1h' | '1d' | '1w' ``` ### `MetricsMinerStatusLogEntry` ```typescript type MetricsMinerStatusLogEntry = { ts: number online: number offline: number sleep: number maintenance: number } ``` ### `MetricsMinerStatusResponse` ```typescript type MetricsMinerStatusResponse = MetricsResponse< MetricsMinerStatusLogEntry, MetricsMinerStatusSummary > ``` ### `MetricsMinerStatusSummary` ```typescript type MetricsMinerStatusSummary = { avgOnline: number | null avgOffline: number | null avgSleep: number | null avgMaintenance: number | null } ``` ### `MetricsPowerModeLogEntry` ```typescript type MetricsPowerModeLogEntry = { ts: number low: number normal: number high: number sleep: number offline: number notMining: number maintenance: number error: number } ``` ### `MetricsPowerModeQueryParams` ```typescript type MetricsPowerModeQueryParams = MetricsQueryParams & { interval?: MetricsInterval } ``` ### `MetricsPowerModeResponse` ```typescript type MetricsPowerModeResponse = MetricsResponse< MetricsPowerModeLogEntry, MetricsPowerModeSummary > ``` ### `MetricsPowerModeSummary` ```typescript type MetricsPowerModeSummary = { avgLow: number | null avgNormal: number | null avgHigh: number | null avgSleep: number | null avgOffline: number | null avgNotMining: number | null avgMaintenance: number | null avgError: number | null } ``` ### `MetricsPowerModeTimelineLogEntry` ```typescript type MetricsPowerModeTimelineLogEntry = { minerId: string container: string segments: MetricsPowerModeTimelineSegment[] } ``` ### `MetricsPowerModeTimelineQueryParams` ```typescript type MetricsPowerModeTimelineQueryParams = { start?: number end?: number container?: string overwriteCache?: boolean } ``` ### `MetricsPowerModeTimelineResponse` ```typescript type MetricsPowerModeTimelineResponse = { log: MetricsPowerModeTimelineLogEntry[] } ``` ### `MetricsPowerModeTimelineSegment` ```typescript type MetricsPowerModeTimelineSegment = { from: number to: number powerMode: string status: string } ``` ### `MetricsQueryParams` Operational metrics v2 API types - shapes returned by `/auth/metrics/*` endpoints. These are the contracts consumers feed back through the reporting-tool base hooks (`useHashrate`, `useEnergyReportSite`, `useOperationsDashboard`, ...). Foundation does not own the RTK Query / HTTP layer itself - see api-integration-todo.md. ```typescript type MetricsQueryParams = { start: number end: number overwriteCache?: boolean } ``` ### `MetricsResponse` ```typescript type MetricsResponse = { log: Log[] summary: Summary } ``` ### `MetricsTemperatureContainerStats` ```typescript type MetricsTemperatureContainerStats = { maxC: number avgC: number } ``` ### `MetricsTemperatureLogEntry` ```typescript type MetricsTemperatureLogEntry = { ts: number containers: Record siteMaxC: number | null siteAvgC: number | null } ``` ### `MetricsTemperatureQueryParams` ```typescript type MetricsTemperatureQueryParams = MetricsQueryParams & { interval?: MetricsInterval container?: string } ``` ### `MetricsTemperatureResponse` ```typescript type MetricsTemperatureResponse = MetricsResponse< MetricsTemperatureLogEntry, MetricsTemperatureSummary > ``` ### `MetricsTemperatureSummary` ```typescript type MetricsTemperatureSummary = { avgMaxTemp: number | null avgAvgTemp: number | null peakTemp: number | null } ``` ### `MetricValueProps` ```typescript type MetricValueProps = { value: number | string unit: string } ``` ### `MicroBTWidgetBoxProps` ```typescript type MicroBTWidgetBoxProps = { data?: Device } ``` ### `MinerActionValue` ```typescript type MinerActionValue = (typeof MINER_ACTIONS)[number] ``` ### `MinerConfig` ```typescript type MinerConfig = { firmware_ver?: string power_mode?: string led_status?: boolean } ``` ### `MinerData` ```typescript type MinerData = { id: string hashrate: MinerHashrate error?: string type?: string info?: { poolConfig?: string } snap?: { config?: { power_mode: string [key: string]: unknown } stats?: { status?: string [key: string]: unknown } } [key: string]: unknown } ``` ### `MinerDeviceSnapshot` ```typescript type MinerDeviceSnapshot = { last?: { snap?: { config?: MinerConfig } } } ``` ### `MinerHashrate` ```typescript type MinerHashrate = { value?: string | number | null unit?: string realValue?: number } ``` ### `MinerHashrateMhs` ```typescript type MinerHashrateMhs = { t_5m?: number } ``` ### `MinerInfo` ```typescript type MinerInfo = { container?: string pos?: string macAddress?: string serialNum?: string } ``` ### `MinerPowerModeTimeline` ```typescript type MinerPowerModeTimeline = { ts?: number miner?: string powerMode?: string data?: { from?: number to?: number } } ``` ### `MinerRecord` ```typescript type MinerRecord = { id?: string shortCode?: string info?: MinerInfo address?: string type?: string alerts?: unknown[] stats?: MinerStats config?: MinerConfig device?: MinerDeviceSnapshot error?: string err?: string isPoolStatsEnabled?: boolean } ``` ### `MinerRow` ```typescript type MinerRow = { id: string code: string unit: string pool: string status?: string } ``` ### `MinersActivityVariant` Visual style for the per-status items. ```typescript type MinersActivityVariant = 'indicators' | 'tiles' ``` ### `MinerSpecificStats` ```typescript type MinerSpecificStats = { upfreq_speed: number [key: string]: unknown } ``` ### `MinersStatusChartData` Pre-shaped data for the miners-status bar chart. ```typescript type MinersStatusChartData = { labels: string[] datasets: MinersStatusDataset[] } ``` ### `MinersStatusDataset` Chart.js dataset for one stacked miners-status series. ```typescript type MinersStatusDataset = { label: string stack: string borderColor: string data: number[] } ``` ### `MinersSummaryBoxProps` ```typescript type MinersSummaryBoxProps = { /** Array of label-value pairs to display in a 2-column grid */ params: MinersSummaryParam[] /** Additional CSS class name */ className?: string } ``` ### `MinersSummaryParam` ```typescript type MinersSummaryParam = { /** Parameter label */ label: string /** Pre-formatted display value (including units) */ value: string } ``` ### `MinerStats` ```typescript type MinerStats = { status?: string uptime_ms?: number power_w?: number hashrate_mhs?: MinerHashrateMhs poolHashrate?: string temperature_c?: { max?: number } } ``` ### `MiningPoolsPanelProps` ```typescript type MiningPoolsPanelProps = Partial<{ /** Override the card title — defaults to `Mining Pools`. */ label: string /** Hide the title row entirely. */ hideHeader: boolean /** Loading state — renders skeleton rows. */ isLoading: boolean /** Number of skeleton rows to sh… ``` ### `MinMaxAvgFormatted` ```typescript type MinMaxAvgFormatted = Partial<{ min: string max: string avg: string }> ``` ### `MinMaxAvgProps` ```typescript type MinMaxAvgProps = MinMaxAvgValues & { className?: string } ``` ### `MinMaxAvgValues` ```typescript type MinMaxAvgValues = Partial<{ min: string max: string avg: string }> ``` ### `MonthlyEbitdaChartProps` ```typescript type MonthlyEbitdaChartProps = { chartData: BarChartDataResult height?: number } ``` ### `MovementData` A single historical device movement: a location/status transition for one device. ```typescript type MovementData = { origin: string destination: string previousStatus: string newStatus: string device?: MovementDevice comments?: ReactNode } ``` ### `MovementDetailsModalProps` Props for `MovementDetailsModal`; pass the selected movement and open/close handlers. ```typescript type MovementDetailsModalProps = Partial<{ isOpen: boolean onClose: () => void movement: MovementData }> ``` ### `MovementDetailsViewModel` Fully resolved, render-ready view of a movement. ```typescript type MovementDetailsViewModel = { device: MovementDeviceView | null origin: MovementSideView destination: MovementSideView comments?: ReactNode } ``` ### `MovementDevice` Device record attached to a movement (e.g. a miner or spare part). All fields optional. ```typescript type MovementDevice = Partial<{ code: string tags: string[] type: string info: Partial<{ subType: string site: string container: string serialNum: string macAddress: string }> }> ``` ### `MovementDeviceView` Render-ready device summary. ```typescript type MovementDeviceView = { code: string model: string attributes: DeviceAttributeView[] } ``` ### `MovementSideView` Resolved label + colors for one side (origin or destination) of a movement. ```typescript type MovementSideView = { locationLabel: string locationColors: BadgeColors statusLabel: string statusColors: BadgeColors } ``` ### `MoveSparePartModalProps` Props for `MoveSparePartModal`. ```typescript type MoveSparePartModalProps = { isOpen?: boolean; onClose?: VoidFunction; sparePart?: MoveSparePartModalSparePart; requestedValues?: { location?: string; status?: string }; locationOptions: FormSelectOption[]; statusOptions: FormSelectOption[]; onSubmit: ( values: { lo… ``` ### `MoveSparePartModalSparePart` A spare part being moved: its display attributes plus current location and status. ```typescript type MoveSparePartModalSparePart = SparePartDetailsRecord & { id: string; location: string; status: string; } ``` ### `MultiLevelSelectRootProps` ```typescript type MultiLevelSelectRootProps = ComponentPropsWithoutRef & { /** * Show a clear button when a value is selected * @default false */ allowClear?: boolean } ``` ### `MultiLevelSelectSectionProps` ```typescript type MultiLevelSelectSectionProps = { open?: boolean children?: ReactNode defaultOpen?: boolean sectionTitle: ReactNode onToggle?: (open: boolean) => void } ``` ### `MultiLevelSelectTriggerProps` ```typescript type MultiLevelSelectTriggerProps = ComponentPropsWithoutRef & { /** * Size of the select trigger * - `sm`: Small (32px height) * - `md`: Medium (36px height) * - `lg`: Large/Big (40px height) * @default 'lg' */ size?: SelectSize /** * Visual… ``` ### `MultiSelectOption` ```typescript type MultiSelectOption = { value: string label: ReactNode disabled?: boolean } ``` ### `MultiSelectProps` ```typescript type MultiSelectProps = { options: MultiSelectOption[] /** Controlled selected values. Omit to use `defaultValue` for uncontrolled mode. */ value?: string[] /** Initial values for uncontrolled mode. Ignored when `value` is provided. */ defaultValue?: string[] onV… ``` ### `MultiSelectSize` ```typescript type MultiSelectSize = 'sm' | 'md' | 'lg' ``` ### `MultiSelectVariant` ```typescript type MultiSelectVariant = 'default' | 'colored' ``` ### `NavigationBlockItem` ```typescript type NavigationBlockItem = { icon: ReactNode title: string description: string navText: string url: string } ``` ### `NotFoundPageProps` ```typescript type NotFoundPageProps = { /** * Callback fired when the "Go Home" button is clicked */ onGoHome?: VoidFunction /** * Page title * @default "404" */ title?: string /** * Message displayed below the title * @default "The page you are looking for does not exist." */… ``` ### `NotificationOptions` ```typescript type NotificationOptions = { /** * Duration in milliseconds (0 = no auto-close) * @default 3000 */ duration?: number /** * Toast position on screen * @default 'top-left' */ position?: ToastPosition /** * Prevent auto-close (keeps toast open until manually closed) *… ``` ### `NotificationVariant` Toast/notification variant options ```typescript type NotificationVariant = 'success' | 'error' | 'warning' | 'info' ``` ### `Nullable` ```typescript type Nullable = T | null ``` ### `OilTempThresholds` ```typescript type OilTempThresholds = { COLD: number LIGHT_WARM: number WARM: number HOT: number SUPERHOT?: number [key: string]: unknown } ``` ### `OperationalDashboardChartId` ```typescript type OperationalDashboardChartId = (typeof OPERATIONAL_DASHBOARD_CHART_IDS)[keyof typeof OPERATIONAL_DASHBOARD_CHART_IDS] ``` ### `OperationalDashboardMinersInput` ```typescript type OperationalDashboardMinersInput = Pick< OperationalMinersStatusChartProps, 'data' | 'isLoading' > ``` ### `OperationalDashboardProps` Props for the operational dashboard composite. ```typescript type OperationalDashboardProps = Partial<{ hashrate: OperationalDashboardTrendInput consumption: OperationalDashboardTrendInput efficiency: OperationalDashboardTrendInput miners: OperationalDashboardMinersInput /** Optional controls (e.g. a date-range picker) rendered abo… ``` ### `OperationalDashboardTrendInput` Data props for one card on the composite (expand state is owned internally). ```typescript type OperationalDashboardTrendInput = Pick ``` ### `OperationalMinersStatusChartProps` Props for the miners-status chart component. ```typescript type OperationalMinersStatusChartProps = Partial<{ data: MinersStatusChartData isLoading: boolean isExpanded: boolean onToggleExpand: VoidFunction }> ``` ### `OperationalTrendChartProps` Shared props for the three line-trend chart components. ```typescript type OperationalTrendChartProps = Partial<{ data: LineChartCardData isLoading: boolean isExpanded: boolean onToggleExpand: VoidFunction }> ``` ### `OperationsEfficiencyMinerTypeInput` ```typescript type OperationsEfficiencyMinerTypeInput = { tailLog?: EfficiencyMinerTypeTailLog | null } ``` ### `OperationsEfficiencyMinerTypeResult` ```typescript type OperationsEfficiencyMinerTypeResult = { chartInput: ToBarChartDataInput isEmpty: boolean } ``` ### `OperationsEfficiencyMinerUnitInput` ```typescript type OperationsEfficiencyMinerUnitInput = { tailLog?: EfficiencyMinerUnitTailLog | null containers?: Container[] } ``` ### `OperationsEfficiencyMinerUnitResult` ```typescript type OperationsEfficiencyMinerUnitResult = { chartInput: ToBarChartDataInput isEmpty: boolean } ``` ### `OperationsEfficiencyProps` ```typescript type OperationsEfficiencyProps = { defaultTab?: EfficiencyTabValue siteView?: EfficiencySiteViewProps minerTypeView?: EfficiencyMinerTypeViewProps minerUnitView?: EfficiencyMinerUnitViewProps } ``` ### `OperationsEnergyChartProps` ```typescript type OperationsEnergyChartProps = { totals: CostSummaryMonetaryTotals | null isLoading?: boolean } ``` ### `OperationsEnergyCostChartData` ```typescript type OperationsEnergyCostChartData = Partial<{ energyCostsUSD: number operationalCostsUSD: number }> ``` ### `OperationsEnergyCostChartProps` ```typescript type OperationsEnergyCostChartProps = Partial<{ title: string unit: string height: number className: string isLoading: boolean emptyMessage: string data: OperationsEnergyCostChartData }> ``` ### `OperationsMinersInput` DI input for the stacked miners-status chart. ```typescript type OperationsMinersInput = Partial<{ log: ReadonlyArray isLoading: boolean error: unknown }> ``` ### `OperationsMinersStatusPoint` Per-day miner status counts, already aggregated by the data layer. ```typescript type OperationsMinersStatusPoint = { ts: number online: number error: number offline: number sleep: number maintenance: number } ``` ### `OperationsMinersViewModel` View-model for the miners-status chart (hook output). ```typescript type OperationsMinersViewModel = { data: MinersStatusChartData isLoading: boolean error: unknown } ``` ### `OperationsTrendInput` DI input for one of the three line-trend charts. ```typescript type OperationsTrendInput = Partial<{ /** Raw metric buckets ordered by time. */ log: ReadonlyArray /** Nominal/reference value in the same base unit; renders a flat reference line. */ nominalValue: number | null isLoading: boolean error: unknow… ``` ### `OperationsTrendPoint` A single timestamped sample of one operational metric (base unit). ```typescript type OperationsTrendPoint = { /** Bucket timestamp in epoch ms. */ ts: number /** Value in its base unit (hashrate: MH/s, power: W, efficiency: W/TH/s). */ value: number } ``` ### `OperationsTrendViewModel` View-model for one line-trend chart (hook output). ```typescript type OperationsTrendViewModel = { data: LineChartCardData isLoading: boolean error: unknown } ``` ### `Optional` ```typescript type Optional = T | undefined ``` ### `OsTypeKey` ```typescript type OsTypeKey = keyof typeof OsTypes ``` ### `OsTypeValue` ```typescript type OsTypeValue = (typeof OsTypes)[OsTypeKey] ``` ### `OverviewChartResult` ```typescript type OverviewChartResult = { yTicksFormatter: (value: number) => string timeRange: TimeRangeType | null datasets: ChartDataset[] } ``` ### `PaginatedResponse` ```typescript type PaginatedResponse = { data: T[] page: number total: number totalPages: number } ``` ### `PaginationParams` ```typescript type PaginationParams = { limit?: number offset?: number page?: number } ``` ### `PaginationProps` ```typescript type PaginationProps = { /** * Current active page number */ current?: number /** * Total number of items */ total?: number /** * Number of items per page * @default 20 */ pageSize?: number /** * Page size options for the select dropdown * @default [10, 20, 50,… ``` ### `ParameterSetting` ```typescript type ParameterSetting = { name: string value?: number | string suffix?: string type?: 'number' | 'string' } ``` ### `ParameterSettings` ```typescript type ParameterSettings = Record ``` ### `ParsedAlertEntry` ```typescript type ParsedAlertEntry = { shortCode: string device: string tags: string[] alertName: string alertCode: string severity: string description?: string message?: string createdAt: string | number status?: string uuid: string id?: string type?: string actions: AlertAc… ``` ### `Pdu` ```typescript type Pdu = { pdu: string power_w?: number | string current_a?: number | string sockets?: PduSocket[] offline?: boolean } ``` ### `PduSocket` ```typescript type PduSocket = { socket: string | number enabled: boolean cooling?: boolean } ``` ### `PendingActionLike` The fields of a staged `actionsStore` submission the tray needs to render a human-readable summary. Mirrors the pool-action payloads enqueued by the add/edit/assign modals. ```typescript type PendingActionLike = { id?: number action?: string params?: Array<{ data?: { poolConfigName?: string /** Pool stratum URLs — present on create/update pool-config actions. */ poolUrls?: Array<{ url?: string }> } poolConfigId?: string configType?: string }> quer… ``` ### `PendingActionsButtonProps` ```typescript type PendingActionsButtonProps = { /** Click handler override — defaults to toggling the actionsStore sidebar. */ onClick?: (event: MouseEvent) => void className?: string } ``` ### `PendingSubmission` ```typescript type PendingSubmission = { id: string | number action: string tags: string[] params: unknown[] [key: string]: unknown } ``` ### `PeriodKey` ```typescript type PeriodKey = keyof typeof PERIOD ``` ### `PeriodValue` ```typescript type PeriodValue = (typeof PERIOD)[PeriodKey] ``` ### `PollingIntervalType` ```typescript type PollingIntervalType = | typeof POLLING_5s | typeof POLLING_1m | typeof POLLING_2m | typeof POLLING_10s | typeof POLLING_15s | typeof POLLING_20s | typeof POLLING_30s ``` ### `PoolConfigData` Structurally identical to `PoolConfigEntry` from `@tetherto/mdk-ui-foundation`. Aliased here so consumers import from `@tetherto/mdk-react-devkit` without reaching into the core layer. ```typescript type PoolConfigData = PoolConfigEntry ``` ### `PoolDetailItem` ```typescript type PoolDetailItem = { title: string value?: string | number } ``` ### `PoolDetailsCardProps` ```typescript type PoolDetailsCardProps = PoolDetailsCardPartialProps & { details: PoolDetailItem[] } ``` ### `PoolEndpoint` ```typescript type PoolEndpoint = { role?: string host: string port: string pool: string url?: string } ``` ### `PoolEndpointFormValues` ```typescript type PoolEndpointFormValues = Omit ``` ### `PoolManagerMinerExplorerProps` ```typescript type PoolManagerMinerExplorerProps = { /** Miners to render in the explorer table. */ miners: ListThingsDevice[] /** Pool configurations powering the "Assign Pool" dropdown. */ poolConfig: PoolConfigData[] /** Called when the operator clicks the "Pool Manager" back link. */ b… ``` ### `PoolManagerPoolsProps` ```typescript type PoolManagerPoolsProps = { /** Array of pool configurations to render. */ poolConfig: PoolConfigData[] /** Called when the operator clicks the "Pool Manager" back link. */ backButtonClick: VoidFunction } ``` ### `PoolManagerProps` ```typescript type PoolManagerProps = { /** Pool configurations shared by every sub-view (Pools, Miner Explorer, Sites). */ poolConfig: PoolConfigData[] /** Dashboard site-level stat blocks. */ stats?: DashboardStats /** Dashboard stats loading flag. */ isStatsLoading?: boolea… ``` ### `PoolManagerSiteOverviewDetailsProps` ```typescript type PoolManagerSiteOverviewDetailsProps = { /** The site (container unit) to render details for. */ unit: UnitData /** Display name shown in the breadcrumb (`Site Overview / `). */ unitName: string /** Pool configurations powering the per-pool detail rows. */ poolConfig:… ``` ### `PoolManagerSitesOverviewProps` ```typescript type PoolManagerSitesOverviewProps = { /** Sites to render (already normalised through `useSitesOverviewData`). */ units: ProcessedContainerUnit[] /** Pool configurations powering each card's pool summary. */ poolConfig: PoolConfigData[] /** Show a skeleton placeholder while… ``` ### `PoolManagerView` Internal surfaces the wrapper switches between on a single route. ```typescript type PoolManagerView = | 'dashboard' | 'pools' | 'sites-overview' | 'miner-explorer' | 'site-detail' ``` ### `PoolSummary` ```typescript type PoolSummary = { id: string name: string description: string units: number miners: number workerName: string | undefined workerPassword: string | undefined endpoints: PoolEndpoint[] validation?: { status: string } credentialsTemplate?: Partial color: string } ``` ### `PowerModeTimelineEntry` ```typescript type PowerModeTimelineEntry = { ts?: number power_mode_group_aggr?: Record status_group_aggr?: Record [key: string]: unknown } ``` ### `PresetItem` ```typescript type PresetItem = { label: string value: DateRange } ``` ### `PressureThresholds` ```typescript type PressureThresholds = { CRITICAL_LOW: number MEDIUM_LOW: number NORMAL?: number MEDIUM_HIGH: number CRITICAL_HIGH: number [key: string]: unknown } ``` ### `ProductionCostChartProps` ```typescript type ProductionCostChartProps = { costLog: ReadonlyArray btcPriceLog: ReadonlyArray dateRange: FinancialDateRange | null isLoading?: boolean } ``` ### `ProfileMenuItem` ```typescript type ProfileMenuItem = { label: string onSelect: () => void /** Render as a destructive action (red). */ danger?: boolean /** Optional leading icon. */ icon?: ReactNode /** Optional secondary line under the label (e.g. "Current: Europe/Podgorica"). */ descriptio… ``` ### `ProfileMenuProps` ```typescript type ProfileMenuProps = { /** Items rendered in the dropdown, top-to-bottom. Defaults to a single "Sign out" item. */ items: ProfileMenuItem[] /** Optional user label rendered at the top of the dropdown (e.g. an email). */ user?: ReactNode /** Override the trigge… ``` ### `PumpItem` ```typescript type PumpItem = { enabled?: boolean index: number } ``` ### `RadioGroupProps` ```typescript type RadioGroupProps = { /** * Layout orientation * @default 'vertical' */ orientation?: 'horizontal' | 'vertical' /** * Remove gap between radio items * @default false */ noGap?: boolean /** * Custom className for the group */ className?: string } & ComponentPr… ``` ### `RadioProps` ```typescript type RadioProps = { /** * Size variant of the radio * @default 'md' */ size?: ComponentSize /** * Color variant when checked * @default 'default' */ color?: ComponentColor /** * Border radius variant (full makes it circular) * @default 'full' */ radius?: Bo… ``` ### `RangeKey` ```typescript type RangeKey = keyof typeof RANGES ``` ### `RangeSelectorOption` ```typescript type RangeSelectorOption = { label: string value: string } ``` ### `RangeSelectorProps` ```typescript type RangeSelectorProps = { options: RangeSelectorOption[] value: string onChange: (value: string) => void className?: string style?: CSSProperties buttonClassName?: string } ``` ### `RangeValue` ```typescript type RangeValue = (typeof RANGES)[RangeKey] ``` ### `RawCsvRow` ```typescript type RawCsvRow = { part: string; model: string; "miner model": string; "serial num": string; mac: string; status: string; location: string; comment: string; } ``` ### `RBACControlSettingsProps` ```typescript type RBACControlSettingsProps = { users: SettingsUser[] roles: RoleOption[] rolePermissions: Record> permissionLabels: Record canWrite: boolean isLoading?: boolean onCreateUser: (data: { name: string; email: string; role:… ``` ### `RepairAction` A single repair action within a batch. `params[0]` carries the part-change payload. ```typescript type RepairAction = Partial<{ params: RepairActionParam[] }> ``` ### `RepairActionParam` Shape of a single inner repair action recorded inside a batch action. `params[0]` holds the part-change payload the sub-row reads. ```typescript type RepairActionParam = Partial<{ comment: string id: string rackId: string info: { parentDeviceId: string | null } }> ``` ### `RepairBatchAction` A repair batch action. Its `params` are the individual repair actions (part swaps, comments, etc.) grouped under one log entry. ```typescript type RepairBatchAction = Partial<{ params: RepairAction[] }> ``` ### `RepairDevice` Minimal device record consumed by `RepairLogChangesSubRow`. The parent fetches these (e.g. via the things API) and passes them in as props. ```typescript type RepairDevice = Partial<{ id: string rack: string info: Partial<{ serialNum: string macAddress: string }> }> ``` ### `RepairLogChangesSubRowProps` ```typescript type RepairLogChangesSubRowProps = { /** * The repair batch action whose part changes should be displayed. */ batchAction: RepairBatchAction /** * Devices referenced by the batch action, pre-fetched by the parent. */ devices: RepairDevice[] /** * Show a spinner while the pa… ``` ### `ReportTimeFrameSelectorProps` ```typescript type ReportTimeFrameSelectorProps = Pick< ReportTimeFrameSelectorState, 'presetTimeFrame' | 'dateRange' | 'setPresetTimeFrame' | 'setDateRange' > ``` ### `ReportTimeFrameSelectorState` ```typescript type ReportTimeFrameSelectorState = { start: Date end: Date presetTimeFrame: number | null dateRange: [Date, Date] setPresetTimeFrame: (value: number | null) => void setDateRange: (value: [Date, Date]) => void } ``` ### `RequireAuthProps` ```typescript type RequireAuthProps = { /** Rendered when a token is present. */ children: ReactNode /** Rendered when no token is present — typically ``. */ fallback: ReactNode /** * When true (default), the current location is persisted to sessionSto… ``` ### `ResponseCodeKey` ```typescript type ResponseCodeKey = keyof typeof RESPONSE_CODE ``` ### `ResponseCodeValue` ```typescript type ResponseCodeValue = (typeof RESPONSE_CODE)[ResponseCodeKey] ``` ### `RevenueChartProps` Props for `RevenueChart`; pass pre-fetched data and optional legend layout overrides. ```typescript type RevenueChartProps = Partial<{ data: RevenueDataItem[] isLoading: boolean siteList: (string | SiteItem)[] legendPosition: Position legendAlign: 'start' | 'center' | 'end' }> ``` ### `RevenueChartViewModel` ```typescript type RevenueChartViewModel = { /** Chart-ready series input — pass to `toBarChartData` in the component layer. */ chartInput: ToBarChartDataInput /** Resolved display unit: `'₿'` (BTC) or `'Sats'` depending on value scale. */ currencyUnit: string } ``` ### `RevenueDataItem` A single time-period revenue record; reserved keys plus one numeric value per site ID. ```typescript type RevenueDataItem = { timeKey: string period: string timestamp: number [key: string]: unknown } ``` ### `RevenueMultiplierKey` ```typescript type RevenueMultiplierKey = keyof typeof REVENUE_MULTIPLIER ``` ### `RevenueSummaryLogEntry` ```typescript type RevenueSummaryLogEntry = { ts: number revenueBTC: number feesBTC: number revenueUSD: number feesUSD: number btcPrice: number powerW: number consumptionMWh: number hashrateMhs: number energyCostsUSD: number operationalCostsUSD: number totalCostsUSD: number ebitdaSe… ``` ### `RevenueSummaryResponse` ```typescript type RevenueSummaryResponse = FinanceResponse ``` ### `RevenueSummaryTotals` ```typescript type RevenueSummaryTotals = { totalRevenueBTC: number totalRevenueUSD: number totalFeesBTC: number totalFeesUSD: number totalCostsUSD: number totalConsumptionMWh: number avgCostPerMWh: number | null avgRevenuePerMWh: number | null avgBtcPrice: number | null avgCurtai… ``` ### `RgbColor` ```typescript type RgbColor = { r: number; g: number; b: number } ``` ### `RouteKey` ```typescript type RouteKey = keyof typeof ROUTE ``` ### `RouteTitleKey` ```typescript type RouteTitleKey = keyof typeof ROUTE_TITLES_MAP ``` ### `RouteTitleValue` ```typescript type RouteTitleValue = (typeof ROUTE_TITLES_MAP)[RouteTitleKey] ``` ### `RouteValue` ```typescript type RouteValue = (typeof ROUTE)[RouteKey] ``` ### `SecondaryLabel` ```typescript type SecondaryLabel = { title: string value: string | number } ``` ### `SelectProps` ```typescript type SelectProps = ComponentPropsWithoutRef & { /** * Show a clear button when a value is selected * @default false */ allowClear?: boolean } ``` ### `SelectSize` ```typescript type SelectSize = 'sm' | 'md' | 'lg' ``` ### `SelectTriggerProps` ```typescript type SelectTriggerProps = ComponentPropsWithoutRef & { /** * Size of the select trigger * - `sm`: Small (32px height) * - `md`: Medium (36px height) * - `lg`: Large/Big (40px height) * @default 'lg' */ size?: SelectSize /** * Visual… ``` ### `SelectVariant` ```typescript type SelectVariant = 'default' | 'colored' ``` ### `SettingsDashboardProps` ```typescript type SettingsDashboardProps = { dangerActions?: ActionButtonProps[] headerControlsProps?: HeaderControlsSettingsProps rbacControlProps?: RBACControlSettingsProps importExportProps?: ImportExportSettingsProps featureFlagsProps?: FeatureFlagsSettingsProps showFeatureFlag… ``` ### `SeverityColorKey` ```typescript type SeverityColorKey = keyof typeof SEVERITY_COLORS ``` ### `SeverityKey` ```typescript type SeverityKey = keyof typeof SEVERITY ``` ### `SeverityLevelKey` ```typescript type SeverityLevelKey = keyof typeof SEVERITY_LEVELS ``` ### `SeverityLevelValue` ```typescript type SeverityLevelValue = (typeof SEVERITY_LEVELS)[SeverityLevelKey] ``` ### `SeverityValue` ```typescript type SeverityValue = (typeof SEVERITY)[SeverityKey] ``` ### `SidebarCallbacks` ```typescript type SidebarCallbacks = Partial<{ onClose: VoidFunction onExpandedChange: (expanded: boolean) => void onItemClick: (item: SidebarMenuItem) => void }> ``` ### `SidebarMenuItem` ```typescript type SidebarMenuItem = SidebarMenuItemBase & SidebarMenuItemOptions ``` ### `SidebarMenuItemBase` ```typescript type SidebarMenuItemBase = { id: string label: string } ``` ### `SidebarMenuItemOptions` ```typescript type SidebarMenuItemOptions = Partial<{ disabled: boolean icon: React.ReactNode items: SidebarMenuItem[] }> ``` ### `SidebarOptions` ```typescript type SidebarOptions = Partial<{ activeId: string expanded: boolean visible: boolean overlay: boolean className: string defaultExpanded: boolean header: React.ReactNode }> ``` ### `SidebarProps` ```typescript type SidebarProps = SidebarOptions & SidebarCallbacks & { items: SidebarMenuItem[] } ``` ### `SignInGoogleButtonProps` ```typescript type SignInGoogleButtonProps = Omit & { /** * Base URL of the OAuth backend (no trailing slash). Click navigates to * `${oauthBaseUrl}/oauth/google`. */ oauthBaseUrl: string /** Override the visible button label. */ label?: string /*… ``` ### `SingleStatCardVariant` ```typescript type SingleStatCardVariant = 'primary' | 'secondary' | 'tertiary' | 'highlighted' ``` ### `SiteItem` A site reference used to resolve site IDs in the data to display names. ```typescript type SiteItem = { id: string name?: string } ``` ### `SiteOverviewDetailsDataOptions` ```typescript type SiteOverviewDetailsDataOptions = { pdus?: Pdu[] connectedMiners?: ListThingsDevice[] connectedMinersData?: Partial containerHashRate?: string actualMinersCount?: number isLoading?: boolean } ``` ### `SitePowerConsumptionSlice` ```typescript type SitePowerConsumptionSlice = { data: Array<{ ts: number; consumption: number }> nominalValue: number | null isLoading: boolean error?: unknown } ``` ### `SiteStatsBarProps` ```typescript type SiteStatsBarProps = { /** Site label rendered in the header row. */ title: string /** Current site-level power consumption, in watts (or whatever `powerUnit` says). */ power?: number /** Display unit for `power` — defaults to `kW`. */ powerUnit?: string /** A… ``` ### `SkeletonBlockProps` ```typescript type SkeletonBlockProps = Partial<{ circle: boolean className: string width: number | string height: number | string borderRadius: number | string }> ``` ### `SocketBorderColorKey` ```typescript type SocketBorderColorKey = keyof typeof SOCKET_BORDER_COLOR ``` ### `SocketProps` ```typescript type SocketProps = { /** Current in amperes */ current_a?: number | null /** Power in watts */ power_w?: number | null /** Whether socket is enabled */ enabled?: boolean /** Socket number/index */ socket?: number | null /** Whether socket is selected */ sele… ``` ### `SparePartDetailsRecord` A spare part's display attributes, rendered by `SparePartDetails`. Extra keys are allowed. ```typescript type SparePartDetailsRecord = { code?: string; type?: string; site?: string; serialNum?: string; macAddress?: string; [key: string]: unknown; } ``` ### `SparePartSubTypesModalPartType` A selectable part type shown as a tab in the subtypes modal. ```typescript type SparePartSubTypesModalPartType = { value: string; label: string; } ``` ### `SparePartSubTypesModalProps` Props for `SparePartSubTypesModal`; the active part type is controlled by the caller. ```typescript type SparePartSubTypesModalProps = { isOpen: boolean; onClose: VoidFunction; partTypes: SparePartSubTypesModalPartType[]; activePartTypeId: string; onPartTypeChange: (id: string) => void; subTypes: string[]; onAddSubType: (name: string) => Promise<{ error?: string } | void>… ``` ### `SpinnerProps` ```typescript type SpinnerProps = { /** * Size variant of the spinner * @default 'md' */ size?: ComponentSize /** * Color variant of the spinner * @default 'primary' */ color?: 'primary' | 'secondary' /** * Whether to display in fullscreen mode * @default false */ fullScre… ``` ### `StagingEnvType` ```typescript type StagingEnvType = typeof STAGING_ENV ``` ### `StatItem` ```typescript type StatItem = { name?: string value?: number | string unit?: string } ``` ### `StatItemType` ```typescript type StatItemType = 'ERROR' | 'SUCCESS' ``` ### `StatKey` ```typescript type StatKey = typeof STAT_REALTIME | typeof STAT_5_MINUTES | typeof STAT_3_HOURS ``` ### `StatsFrequencyMhz` ```typescript type StatsFrequencyMhz = { avg: number chips: ChipData[] [key: string]: unknown } ``` ### `StatsTemperatureC` ```typescript type StatsTemperatureC = { avg: number min: number max: number chips: TempChipData[] [key: string]: unknown } ``` ### `Status` ```typescript type Status = 'idle' | 'loading' | 'success' | 'error' ``` ### `StatusVariant` Status variants for state indication Used for badges, notifications, and status indicators ```typescript type StatusVariant = 'success' | 'processing' | 'error' | 'warning' | 'default' | 'idle' ``` ### `SubsidyFeesLogEntry` ```typescript type SubsidyFeesLogEntry = { ts: number blockReward: number blockTotalFees: number avgFeesSatsVByte?: number | null } ``` ### `SubsidyFeesResponse` ```typescript type SubsidyFeesResponse = FinanceResponse ``` ### `SubsidyFeesTotals` ```typescript type SubsidyFeesTotals = { totalBlockReward: number totalBlockTotalFees: number avgBlockReward: number | null avgBlockTotalFees: number | null } ``` ### `SupplyLiquidBoxContainerSettings` ```typescript type SupplyLiquidBoxContainerSettings = { thresholds?: Record } ``` ### `SupplyLiquidBoxProps` ```typescript type SupplyLiquidBoxProps = { data?: Device containerSettings?: SupplyLiquidBoxContainerSettings | null } ``` ### `SwitchProps` ```typescript type SwitchProps = { /** * Size variant of the switch * @default 'md' */ size?: ComponentSize /** * Color variant when checked * @default 'default' */ color?: ComponentColor /** * Border radius variant * @default 'none' */ radius?: BorderRadius /** * Custom… ``` ### `TableColorKey` ```typescript type TableColorKey = keyof typeof TABLE_COLORS ``` ### `TagFilterBarProps` ```typescript type TagFilterBarProps = { filterTags: string[] localFilters: AlertLocalFilters onSearchTagsChange: (tags: string[]) => void onLocalFiltersChange: (filters: AlertLocalFilters) => void /** * Site-specific overrides for the "type" filter children. * If provided, the… ``` ### `TagInputDropdownProps` ```typescript type TagInputDropdownProps = { /** Filtered options to display */ filteredOptions: TagInputOption[] /** Current tags (selected values) - use to show selected state in custom dropdown */ selectedTags: string[] /** Index of the currently highlighted option (for keyboard… ``` ### `TagInputOption` ```typescript type TagInputOption = string | { value: string; label: string; disabled?: boolean } ``` ### `TagInputProps` ```typescript type TagInputProps = { /** * Controlled tags (array of tag values) */ value?: string[] /** * Callback when tags change (add/remove) */ onTagsChange?: (tags: string[]) => void /** * Callback when input value changes (typing). Receives current input value. Usefu… ``` ### `TagInputRef` ```typescript type TagInputRef = { /** Clear the input value programmatically */ clearInputValue: VoidFunction /** Focus the input */ focus: VoidFunction /** Blur the input */ blur: VoidFunction /** Get current input value */ getInputValue: () => string } ``` ### `TagProps` ```typescript type TagProps = { /** * Color variant of the tag * @default 'dark' */ color?: 'dark' | 'red' | 'green' | 'amber' | 'blue' /** * Custom className for the root element */ className?: string /** * Children content */ children?: ReactNode } & ComponentPropsWi… ``` ### `Tank` ```typescript type Tank = { cold_temp_c: number enabled: boolean color?: string flash?: boolean tooltip?: string } ``` ### `TankRowPressure` ```typescript type TankRowPressure = Partial<{ value: number flash: boolean color: string tooltip: string }> ``` ### `TankRowProps` ```typescript type TankRowProps = { label: string temperature: number unit: string oilPumpEnabled: boolean waterPumpEnabled: boolean color: string flash?: boolean tooltip?: string pressure: TankRowPressure } ``` ### `TanksBoxPressure` ```typescript type TanksBoxPressure = { value?: number flash?: boolean color?: string tooltip?: string } ``` ### `TanksBoxProps` ```typescript type TanksBoxProps = { data?: { oil_pump: Tank[] water_pump: WaterPump[] pressure: TanksBoxPressure[] } } ``` ### `TempChipData` ```typescript type TempChipData = { index: number max?: number min?: number avg?: number } ``` ### `TemperatureColorKey` ```typescript type TemperatureColorKey = keyof typeof TEMPERATURE_COLORS ``` ### `TextAlign` Text alignment options Used by Typography component ```typescript type TextAlign = 'left' | 'center' | 'right' | 'justify' ``` ### `TextAreaProps` ```typescript type TextAreaProps = ComponentProps<'textarea'> & { /** * Optional label displayed above the textarea */ label?: string /** * HTML id for the textarea. Required when using label for accessibility. */ id?: string /** * Validation error message. When provided, d… ``` ### `Theme` ```typescript type Theme = 'light' | 'dark' | 'system' ``` ### `ThemeConfig` ```typescript type ThemeConfig = { defaultTheme?: Theme storageKey?: string } ``` ### `ThingActionValue` ```typescript type ThingActionValue = (typeof THING_ACTIONS)[number] ``` ### `ThresholdBarChartInput` ```typescript type ThresholdBarChartInput = { labels: string[] series: Array<{ label: string values: number[] color?: string stack?: string }> } ``` ### `ThresholdLineChartData` ```typescript type ThresholdLineChartData = { series: ThresholdLineChartSeries[] thresholds?: ThresholdLineChartThreshold[] } ``` ### `ThresholdLineChartInput` ```typescript type ThresholdLineChartInput = { series: Array<{ label: string points: Array<{ ts: number; value: number }> color?: string }> constants?: Array<{ label: string value: number color?: string style?: { borderDash?: number[] } }> } ``` ### `ThresholdLineChartPoint` ```typescript type ThresholdLineChartPoint = { value: number timestamp: string | number } ``` ### `ThresholdLineChartProps` ```typescript type ThresholdLineChartProps = Partial<{ title: string unit: string height: number /** When true, uses a taller default height (360px). */ isTall: boolean className: string emptyMessage: string isLegendVisible: boolean data: ThresholdLineChartData yTicksFormatter: (valu… ``` ### `ThresholdLineChartSeries` ```typescript type ThresholdLineChartSeries = { label: string color?: string fill?: boolean points: ThresholdLineChartPoint[] } ``` ### `ThresholdLineChartThreshold` ```typescript type ThresholdLineChartThreshold = { label: string value: number color?: string } ``` ### `TimeFormatType` ```typescript type TimeFormatType = typeof TIME_FORMAT ``` ### `TimeframeControlsDateRange` ```typescript type TimeframeControlsDateRange = { start: number end: number } ``` ### `TimeframeControlsOnRangeChange` ```typescript type TimeframeControlsOnRangeChange = ( range: [Date, Date], options: Partial<{ year: number; month: number; period: string }>, ) => void ``` ### `TimeframeControlsProps` ```typescript type TimeframeControlsProps = Partial<{ hint: string onReset: VoidFunction showResetButton: boolean isWeekSelectVisible: boolean isMonthSelectVisible: boolean layout: 'horizontal' | 'stacked' dateRange: TimeframeControlsDateRange timeframeType: TimeframeTypeValue | nul… ``` ### `TimeframeTypeKey` ```typescript type TimeframeTypeKey = keyof typeof TIMEFRAME_TYPE ``` ### `TimeframeTypeValue` ```typescript type TimeframeTypeValue = (typeof TIMEFRAME_TYPE)[TimeframeTypeKey] ``` ### `TimeframeWeekFlatContentProps` ```typescript type TimeframeWeekFlatContentProps = { visibleWeeks: ReturnType } ``` ### `TimeframeWeekTreeContentProps` ```typescript type TimeframeWeekTreeContentProps = { timezone: string selectedYear: number selectedMonth: number } ``` ### `TimeInterval` ```typescript type TimeInterval = { start: number end: number } ``` ### `TimeKey` ```typescript type TimeKey = keyof typeof TIME ``` ### `TimelineChartData` ```typescript type TimelineChartData = { labels: string[] datasets: TimelineChartDataset[] } ``` ### `TimelineChartDataPoint` ```typescript type TimelineChartDataPoint = { x: [number, number] y: string | undefined } ``` ### `TimelineChartDataset` ```typescript type TimelineChartDataset = { label: string data: TimelineChartDataPoint[] borderColor?: string[] backgroundColor?: string[] color?: string } ``` ### `TimelineChartProps` ```typescript type TimelineChartProps = { initialData: TimelineChartData newData?: TimelineChartData skipUpdates?: boolean range?: ChartRange axisTitleText?: AxisTitleText isLoading?: boolean title?: string height?: number } ``` ### `TimelineItemData` ```typescript type TimelineItemData = { item: AlarmItemData dot: ReactNode children: ReactNode } ``` ### `TimelineOption` ```typescript type TimelineOption = { value: string label: string disabled?: boolean } ``` ### `TimelineSelectorProps` ```typescript type TimelineSelectorProps = { /** Currently selected timeline value (e.g. `'1m'`, `'5m'`). */ value: string /** Called whenever the user picks a new option. */ onChange: (next: string) => void /** * Available options — defaults to {@link getTimelineOptions}. Pass a c… ``` ### `TimeRangeFormatted` ```typescript type TimeRangeFormatted = { start: string end: string formatted: string } ``` ### `TimeRangeType` ```typescript type TimeRangeType = (typeof TimeRangeTypes)[keyof typeof TimeRangeTypes] ``` ### `TimeSelection` ```typescript type TimeSelection = { end: Date start: Date year: number month?: number label?: string } ``` ### `TIncidentRowProps` ```typescript type TIncidentRowProps = { id: string title: string body: string subtitle: string severity: TIncidentSeverity onClick?: (id: string) => void } ``` ### `ToastPosition` ```typescript type ToastPosition = | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center' ``` ### `ToastVariant` ```typescript type ToastVariant = NotificationVariant ``` ### `TransactionEntry` ```typescript type TransactionEntry = { changed_balance?: number satoshis_net_earned?: number fees_colected_satoshis?: number mining_extra?: { tx_fee?: number } } ``` ### `TransactionSum` ```typescript type TransactionSum = { revenueBTC: number feesBTC: number } ``` ### `TypographyColor` Typography color options (includes muted variant) ```typescript type TypographyColor = 'default' | 'primary' | 'success' | 'warning' | 'error' | 'muted' ``` ### `TypographyProps` ```typescript type TypographyProps = { /** * Typography variant * @default 'body' */ variant?: 'heading1' | 'heading2' | 'heading3' | 'body' | 'secondary' | 'caption' /** * Text size * @default undefined (uses variant default) */ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl… ``` ### `UnitData` ```typescript type UnitData = { last?: UnitLast type?: string info?: UnitInfo [key: string]: unknown } ``` ### `UnitKey` ```typescript type UnitKey = keyof typeof UNITS ``` ### `UnitLabel` ```typescript type UnitLabel = 'decimal' | 'k' | 'M' | 'G' | 'T' | 'P' ``` ### `UnitValue` ```typescript type UnitValue = (typeof UNITS)[UnitKey] ``` ### `UpdateExistedActionsParams` ```typescript type UpdateExistedActionsParams = { actionType: string pendingSubmissions: PendingSubmission[] selectedDevices: Device[] } ``` ### `UseCabinetDetailResult` ```typescript type UseCabinetDetailResult = { /** Whether a cabinet is selected at all. */ hasSelection: boolean /** Cabinet display title (`LV Cabinet 1` / transformer title). */ title: string /** The non-root powermeter reading rows. */ powerMeters: CabinetReadingRow[] /** The cab… ``` ### `UseContainerThresholdsProps` ```typescript type UseContainerThresholdsProps = { data: UnknownRecord onSave?: (params: { data: UnknownRecord }) => Promise } ``` ### `UseContainerThresholdsReturn` ```typescript type UseContainerThresholdsReturn = { // State thresholds: UnknownRecord parameters: UnknownRecord isEditing: boolean isSaving: boolean isSiteLoading: boolean isSettingsLoading: boolean // Methods handleThresholdChange: (thresholdType: string, key: string, value: string | nu… ``` ### `UseContainerWidgetsDataOptions` ```typescript type UseContainerWidgetsDataOptions = UseContainerWidgetsOptions ``` ### `UseContainerWidgetsDataResult` ```typescript type UseContainerWidgetsDataResult = { /** Card-ready data for every container, one entry per widget card. */ containers: ContainerWidgetItem[] isLoading: boolean /** Human-readable message when either underlying query errors, else undefined. */ errorMessage?: string /** True… ``` ### `UseCostSummaryOptions` ```typescript type UseCostSummaryOptions = UseFinancialDateRangeOptions & { query?: CostSummaryQueryResult } ``` ### `UseDeviceAlarmsResult` ```typescript type UseDeviceAlarmsResult = { /** Timeline items ready for the detail panel's "Active Alarms" box. */ alarmsDataItems: TimelineItemData[] /** Total number of alarms across the selected devices. */ alarmsCount: number } ``` ### `UseEbitdaOptions` ```typescript type UseEbitdaOptions = UseFinancialDateRangeOptions & { ebitda?: EbitdaResponse | undefined isLoading?: boolean fetchErrors?: string[] } ``` ### `UseEnergyBalanceOptions` ```typescript type UseEnergyBalanceOptions = { data?: EnergyBalanceResponse isLoading?: boolean fetchErrors?: string[] dateRange: FinancialDateRange | null availablePowerMW: number } ``` ### `UseEnergyReportSiteInput` ```typescript type UseEnergyReportSiteInput = { consumptionError?: unknown tailLogLoading?: boolean containersLoading?: boolean consumptionLoading?: boolean consumptionFetching?: boolean nominalConfigLoading?: boolean dateRange: EnergyReportDateRange containers?: EnergyReportContainer… ``` ### `UseEnergyReportSiteResult` ```typescript type UseEnergyReportSiteResult = { isLoading: boolean powerModeData: PowerModeTableRow[] refetchSnapshotData?: VoidFunction containers: EnergyReportContainer[] tailLogData: EnergyReportTailLogItem[][] tailLogHead: EnergyReportTailLogItem | undefined miningUnitCards: Energ… ``` ### `UseExplorerDataOptions` ```typescript type UseExplorerDataOptions = { /** Which tab's things to fetch and shape. */ deviceType: DeviceExplorerDeviceType /** Free-text search chips from the toolbar (OR-matched against each row). */ searchTags?: string[] /** Active filter selections from the toolbar cascader… ``` ### `UseExplorerDataResult` ```typescript type UseExplorerDataResult = { /** Rows for the active tab, after search + filter, ready for ``. */ data: DeviceExplorerDeviceData[] /** Autocomplete options for the search box, derived from the fetched rows. */ searchOptions: DeviceExplorerSearchOptio… ``` ### `UseExplorerSelectionParams` ```typescript type UseExplorerSelectionParams = { /** The active Explorer tab — decides which `devicesStore` setters fire. */ deviceType: DeviceExplorerDeviceType /** All rows currently shown for the tab (the search/filter output). */ rows: DeviceExplorerDeviceData[] /** The table's row… ``` ### `UseExplorerSelectionResult` ```typescript type UseExplorerSelectionResult = { /** The resolved selected rows (post search/filter). */ selectedRows: DeviceExplorerDeviceData[] /** Whether the container detail snapshots are still loading. */ isLoading: boolean } ``` ### `UseExplorerThingDetailResult` ```typescript type UseExplorerThingDetailResult = { /** Whether an id was provided at all — drives the "select a row" empty state. */ hasSelection: boolean /** Display name of the selected thing (container name / pos / id fallback). */ title: string /** Raw status string, or `offline`. Em… ``` ### `UseFinancialDateRangeOptions` ```typescript type UseFinancialDateRangeOptions = { defaultPeriod?: PeriodValue timezone?: string } ``` ### `UseFinancialDateRangeResult` ```typescript type UseFinancialDateRangeResult = { dateRange: FinancialDateRange | null handleRangeChange: ( dates: [Date, Date] | null, rangeOptions?: FinancialRangeChangeOptions, ) => void onDateRangeReset: () => void datePicker: ReactElement } ``` ### `UseFormFieldReturn` Hook to access the current form field's state and generated IDs. Must be used inside a `<FormField>` and `<FormItem>`. ```typescript type UseFormFieldReturn = ReturnType> & { id: string name: string formItemId: string formDescriptionId: string formMessageId: string } ``` ### `UseFormResetOptions` ```typescript type UseFormResetOptions = { /** * The form instance from useForm() */ form: UseFormReturn /** * Optional callback called before reset */ onBeforeReset?: VoidFunction /** * Optional callback called after reset */ onAfterReset?: VoidFunction } ``` ### `UseFormResetReturn` ```typescript type UseFormResetReturn = { /** * Reset the form to default values */ resetForm: VoidFunction /** * Whether the form has been modified from its default values */ isDirty: boolean } ``` ### `UseHashBalanceDerived` ```typescript type UseHashBalanceDerived = { isEmpty: boolean periodType: PeriodType showCombinedCostChart: boolean isNetworkHashrateEmpty: boolean costMetrics: HashBalanceMetric[] showWeeklyCostDisclaimer: boolean filteredLog: HashRevenueLogEntry[] revenueMetrics: HashBalanceMetri… ``` ### `UseHashBalanceInput` ```typescript type UseHashBalanceInput = { log?: HashRevenueLogEntry[] currency: HashBalanceCurrency dateRange: FinancialDateRange data?: HashRevenueResponse | null timeframeType?: TimeframeTypeValue | null } ``` ### `UseHashrateOptions` ```typescript type UseHashrateOptions = { /** * Result of fetching the v2 `/auth/metrics/hashrate?groupBy=...` endpoint. * * Wire whichever data layer you use (RTK Query, TanStack Query, fixtures) * and pass the result here - this hook never fetches itself. */ query?: HashrateQu… ``` ### `UseHashrateResult` ```typescript type UseHashrateResult = { log: HashrateGroupedLog | undefined isLoading: boolean error: unknown } ``` ### `UseListViewFiltersParams` ```typescript type UseListViewFiltersParams = { site?: string selectedType?: string availableDevices: AvailableDevices typeFiltersForSite: CascaderOption[] } ``` ### `UseMinerDetailResult` ```typescript type UseMinerDetailResult = { /** The selected miners (post bridge dispatch). */ miners: Device[] /** The first selected miner — drives the info / chips cards. */ headMiner?: Device /** Label/value rows for {@link MinerInfoCard}. */ infoItems: InfoItem[] /** The head… ``` ### `UseOperationsDashboardInput` Aggregate DI input for `useOperationsDashboard`. ```typescript type UseOperationsDashboardInput = Partial<{ hashrate: OperationsTrendInput consumption: OperationsTrendInput efficiency: OperationsTrendInput miners: OperationsMinersInput }> ``` ### `UseOperationsDashboardResult` Full view-model returned by `useOperationsDashboard`. ```typescript type UseOperationsDashboardResult = { hashrate: OperationsTrendViewModel consumption: OperationsTrendViewModel efficiency: OperationsTrendViewModel miners: OperationsMinersViewModel } ``` ### `UsePoolConfigsResult` ```typescript type UsePoolConfigsResult = { pools: PoolSummary[] poolIdMap: Record isLoading: boolean error: unknown } ``` ### `UseSiteOverviewDetailsDataResult` ```typescript type UseSiteOverviewDetailsDataResult = { actualMinersCount: number containerHashRate: string pdus: Pdu[] segregatedPduSections: Record minersHashmap: Record connectedMiners: ListThingsDevice[] | undefined containerInfo: ContainerInfo connectedM… ``` ### `UseSubsidyFeesInput` ```typescript type UseSubsidyFeesInput = { log?: SubsidyFeesLogEntry[] data?: SubsidyFeesResponse | null dateRange: FinancialDateRange | null } ``` ### `UseTimeframeControlsParams` ```typescript type UseTimeframeControlsParams = Pick< TimeframeControlsProps, 'dateRange' | 'timeframeType' | 'onRangeChange' | 'onTimeframeTypeChange' > & { isWeekSelectVisible: boolean /** Both month + week selects visible → week nested tree. */ weekTree: boolean } ``` ### `ValidateCSVRecordsOptions` ```typescript type ValidateCSVRecordsOptions = { checkDuplicateDelegate: (params: { rackId: string; serialNum: string[]; macAddress?: string[]; }) => Promise; rackIds: Record; validLocations?: string[]; subPartTypes?: Record>; minerModels?… ``` ### `ValueUnit` Shared type definitions for utility functions ```typescript type ValueUnit = { value: number | string | null unit: string realValue: number } ``` ### `WaterPump` ```typescript type WaterPump = { enabled: boolean } ``` ### `WebappUrlsKey` ```typescript type WebappUrlsKey = keyof typeof WEBAPP_URLS ``` ### `WebappUrlsValue` ```typescript type WebappUrlsValue = (typeof WEBAPP_URLS)[WebappUrlsKey] ``` ### `WeekData` ```typescript type WeekData = { start: Date end: Date label?: string bucketYear?: number bucketMonth?: number disabled?: boolean } ``` ### `WeekRangeCacheEntry` ```typescript type WeekRangeCacheEntry = { start: Date end: Date disabled?: boolean } ``` ### `WeightedAverageResult` ```typescript type WeightedAverageResult = { avg: number totalWeight: number weightedValue: number } ``` ### `WeightedDataPoint` ```typescript type WeightedDataPoint = { value: number weight: number } ``` ### `WidgetTopRowProps` ```typescript type WidgetTopRowProps = { title: string power?: number unit?: string statsErrorMessage?: string | ErrorWithTimestamp[] | null alarms?: AlarmsMap className?: string } ``` # Foundation Dashboard Types (/reference/ui/types/foundation-dashboard) Type definitions for dashboard data structures. ## Package `@tetherto/mdk-ui-foundation` ## Types @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ChartCardData` Minimum chart-ready payload — assignable to `LineChartCardData` from `@tetherto/mdk-react-devkit`. ```typescript type ChartCardData = { datasets: ChartDataset[] } & Partial<{ /** Y-axis tick formatter (e.g. `(v) => \`${v.toFixed(2)} MW\``). */ yTicksFormatter: (value: number) => string /** Cr… ``` ### `ChartDataPoint` A single (x, y) sample. `x` is a Unix timestamp in **seconds** (lightweight-charts convention); `y` is `null` to render gaps. ```typescript type ChartDataPoint = { x: number y: number | null } ``` ### `ChartDataset` A named line series with colour and points. Optional `visible` lets pages pre-hide datasets without removing them from the data array. ```typescript type ChartDataset = { label?: string borderColor: string data: ChartDataPoint[] visible?: boolean } ``` # Foundation General Types (/reference/ui/types/foundation-general) General type definitions from the ui-foundation package. ## Package `@tetherto/mdk-ui-foundation` ## Types @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ApiError` Shared error contract used across HTTP/API surfaces. ```typescript type ApiError = { error: string message: string status: number data?: { message?: string } } ``` ### `ImportResult` ```typescript type ImportResult = { success: boolean applied?: string[] errors?: string[] message?: string } ``` ### `PermLevel` ```typescript type PermLevel = 'rw' | 'r' | false ``` ### `RoleOption` ```typescript type RoleOption = { label: string value: string } ``` ### `RolesPermissionsData` ```typescript type RolesPermissionsData = { permissions: Record> labels: Record } ``` ### `SettingsExportData` ```typescript type SettingsExportData = { headerControls?: Record featureFlags?: Record timestamp?: string version?: string } & TExtra ``` ### `SettingsUser` ```typescript type SettingsUser = { id: string name?: string email: string role: string last_login?: string lastActive?: string [key: string]: unknown } ``` ### `SubscriberCallback` ```typescript type SubscriberCallback = (value: T) => void ``` ### `UnknownRecord` Generic type for objects with unknown structure. ```typescript type UnknownRecord = Record ``` ### `Unsubscribe` ```typescript type Unsubscribe = () => void ``` # Utilities (/reference/ui/utilities) Helper functions, formatters, and utilities exported from `@tetherto/mdk-ui-foundation`. ## Browse by category | Category | Description | |----------|-------------| | [Alerts](/reference/ui/utilities/alerts) | Alert query builders and time-range utilities | | [Auth](/reference/ui/utilities/auth) | Authentication helper functions | | [Dashboard](/reference/ui/utilities/dashboard) | Dashboard data utilities | | [General](/reference/ui/utilities/general) | General-purpose utilities | | [Utils](/reference/ui/utilities/utils) | Core utility functions | ## Import pattern ```tsx ``` # Alert Utilities (/reference/ui/utilities/alerts) Utilities for building alert queries and managing time ranges. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation ## Default historical range ```tsx ``` ### When to use `getDefaultHistoricalAlertsRange` Use `getDefaultHistoricalAlertsRange` to initialise an alert-history view with the standard 14-day look-back window. Pass an explicit `now` value when the range must be deterministic, such as in a test. ### `getDefaultHistoricalAlertsRange` example ```tsx const initialRange = getDefaultHistoricalAlertsRange() const testRange = getDefaultHistoricalAlertsRange(1_700_000_000_000) ``` @tetherto/mdk-ui-foundation ## Build the current-alert query ```tsx ``` ### When to use `buildCurrentAlertDevicesParams` Use `buildCurrentAlertDevicesParams` to construct the `list-things` request for devices that currently carry alerts. Pass active search tags when the server should narrow the current-alert result before client-side presentation. ### `buildCurrentAlertDevicesParams` example ```tsx const allCurrentAlerts = buildCurrentAlertDevicesParams() const matchingAlerts = buildCurrentAlertDevicesParams([ 'ip-192.168.1.1', 'sn-ABC123', ]) ``` @tetherto/mdk-ui-foundation ## Time intervals ```tsx ``` ## Related API - [**`fetchHistoricalAlertsInChunks`**](/reference/ui/utilities/alerts/#fetchhistoricalalertsinchunks) - [**`mergeAlertsByUuid`**](/reference/ui/utilities/alerts/#mergealertsbyuuid) ### When to use `breakTimeIntoIntervals` Use `breakTimeIntoIntervals` when a historical request must be split into bounded windows before fetching. Alert history uses this to avoid requesting a large time range in one call. ### Historical-alert chunking workflow Create the windows with `breakTimeIntoIntervals`, fetch them from oldest to newest, then combine overlapping alert results with `mergeAlertsByUuid`. `fetchHistoricalAlertsInChunks` composes that workflow when its default error and abort behaviour fits the caller. ### Range behaviour The function returns an empty array when the bounds are non-finite, when `start >= end`, or when `intervalMs <= 0`. The final interval is clamped to `end`, so it may be shorter than `intervalMs`; callers must not assume that every window has equal duration. ### `breakTimeIntoIntervals` example ```tsx breakTimeIntoIntervals, mergeAlertsByUuid, ONE_DAY_MS, } from '@tetherto/mdk-ui-foundation' let alerts = [] for (const window of breakTimeIntoIntervals(start, end, ONE_DAY_MS)) { const next = await fetchAlerts(window) alerts = mergeAlertsByUuid(alerts, next) } ``` @tetherto/mdk-ui-foundation ## Fetch historical alerts ```tsx ``` ## Related API - [**`fetchHistoricalAlertsInChunks`**](/reference/ui/utilities/alerts/#fetchhistoricalalertsinchunks) - [**`buildHistoricalAlertsParams`**](/reference/ui/utilities/alerts/#buildhistoricalalertsparams) - [**`mergeAlertsByUuid`**](/reference/ui/utilities/alerts/#mergealertsbyuuid) ### When to use the historical-alert workflow Use `fetchHistoricalAlertsInChunks` for a full alert-history range rather than issuing one unbounded request. It coordinates the range splitting and result merging; `buildHistoricalAlertsParams` adapts each window to the Gateway query. ### Historical-alert fetch workflow Start with the requested range, let `fetchHistoricalAlertsInChunks` visit each window from oldest to newest, and build one `history-log` request per callback. The helper uses `mergeAlertsByUuid` between windows. Call `mergeAlertsByUuid` again only when integrating the returned range into alerts already held by the caller. ### Historical-alert failure and abort behaviour Individual window failures are swallowed so one bad request does not discard the rest of the range. An abort signal is checked between windows rather than during the caller's active request; the request function must use that signal as well if it needs in-flight cancellation. ### Historical-alert workflow example ```tsx buildHistoricalAlertsParams, fetchHistoricalAlertsInChunks, mergeAlertsByUuid, } from '@tetherto/mdk-ui-foundation' const fetchedAlerts = await fetchHistoricalAlertsInChunks( range, async (window) => { const params = buildHistoricalAlertsParams(window) const result = await gateway.historyLog(params) return result.data }, { signal: abortController.signal }, ) const allAlerts = mergeAlertsByUuid(existingAlerts, fetchedAlerts) ``` @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `breakTimeIntoIntervals` Split `[start, end]` into consecutive windows of `intervalMs`. The final window is clamped to `end`. Returns an empty array when the range is empty or inverted. Mirrors Mining OS's `breakTimeIntoIntervals`. **Function** ```typescript (start: number, end: number, intervalMs: number = ONE_DAY_MS) => TimeInterval[] ``` ### `buildCurrentAlertDevicesParams` `list-things` params for the current-alerts table: every device that currently carries one or more alerts, with the fields the `<CurrentAlerts>` table reads. Consumed by `useCurrentAlertDevices`. **Function** ```typescript (filterTags: string[] = []) => ListThingsParams ``` ### `buildHistoricalAlertsParams` `history-log` params for a single alerts window. The chunked fetch (`useHistoricalAlerts`) calls this once per 24h sub-window. **Function** ```typescript (range: HistoricalAlertsRange) => HistoryLogParams ``` ### `fetchHistoricalAlertsInChunks` Fetch a historical-alerts range as successive 24h windows, merging the results by `uuid`. `fetchWindow` is called once per window (oldest → newest); individual window failures are swallowed (matches Mining OS) so one bad window doesn't dro… **Function** ```typescript (range: { start: number; end: number }, fetchWindow: (window: TimeInterval) => Promise, options: FetchHistoricalAlertsOptions = {}) => Promise ``` ### `getAlertsForDevices` Flatten an array of devices into a list of incident rows, one per alert. Devices without `last.alerts` are skipped. The output is **not yet sorted**; pair with `sortIncidentsBySeverity` for the final list-view order. **Function** ```typescript (devices: ListThingsDevice[], formatDate: (d: Date) => string = (d) => d.toISOString()) => IncidentRow[] ``` ### `getDefaultHistoricalAlertsRange` Default historical-alerts range: the last `DEFAULT_HISTORICAL_WINDOW_MS` ending now. Used by the devkit `<Alerts>` feature and the shell Alerts page to seed their range state. **Function** ```typescript (now: number = Date.now()) => HistoricalAlertsRange ``` ### `mapDevicesToIncidents` One-shot helper: `devices → sorted rows`. Used by the `useActiveIncidents` hook's `select` projection. **Function** ```typescript (devices: ListThingsDevice[], formatDate?: (d: Date) => string) => IncidentRow[] ``` ### `mapHistoryLogToAlerts` Normalise raw `history-log` alert rows into the shape the devkit `<HistoricalAlerts>` table consumes. The table derives its device label, short code, and filter tokens from each row's `thing` (treated as a device), so this guarantees `thin… **Function** ```typescript (rows: HistoricalAlert[] = []) => HistoricalAlert[] ``` ### `mergeAlertsByUuid` Concatenate `next` onto `prev`, replacing any row that shares a `uuid` (later windows win) and appending the rest. Rows without a `uuid` are always appended. Mirrors Mining OS's `updateHistoricalData`. **Function** ```typescript (prev: T[], next: T[]) => T[] ``` ### `sortIncidentsBySeverity` Sort rows by severity (critical → high → medium), then by `id` for deterministic ordering when severities tie. Returns a new array. **Function** ```typescript (rows: IncidentRow[]) => IncidentRow[] ``` # Auth Utilities (/reference/ui/utilities/auth) Helper functions for authentication flows. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `extractAuthTokenFromUrl` Extract `?authToken=` from a URL search string. Accepts either a full URL, a query string with leading `?`, or a bare query string. **Function** ```typescript (search: string) => string | null ``` ### `stripAuthTokenFromUrl` Build a URL string with the `?authToken=` parameter stripped. Used after the OAuth callback to remove the token from the address bar without losing any other query state. **Function** ```typescript (search: string) => string ``` # Dashboard Utilities (/reference/ui/utilities/dashboard) Utilities for dashboard data processing. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `buildHashrateTailLogParams` Hashrate tail-log params — per-miner 1-minute aggregate, summed across the `t-miner` tag. **Function** ```typescript (range: DashboardQueryRange) => TailLogParams ``` ### `buildMinerpoolStatsHistoryExtDataParams` Ext-data params for `type=minerpool, key=stats-history` — per-pool hashrate snapshots over time. Pair with `extDataQuery` to feed the multi-series Hash Rate chart (Mining OS + Aggr Pool + per-pool lines). **Function** ```typescript (range: MinerpoolStatsHistoryRange = {}) => ExtDataParams ``` ### `buildSiteConsumptionTailLogParams` Site-level consumption tail-log params — reads the dedicated powermeter's `site_power_w` aggregate (Mining OS's `type=powermeter, tag=t-powermeter, aggrFields={site_power_w:1}` query). Returns the same series the header's `useSitePowerMete… **Function** ```typescript (range: DashboardQueryRange) => TailLogParams ``` ### `DEFAULT_TIMELINE_OPTIONS` Canonical short-form intervals exposed by MDK UI Shell. The keys map onto the `key=stat-<value>` query parameter expected by `GET /auth/tail-log`. **Constant** ```typescript readonly TimelineOption[] ``` ### `getTimelineOptions` Default options for the dashboard timeline selector. Mirrors Mining OS's `timelineRadioButtons` (5m / 30m / 3h / 1D) — the production dashboard doesn't expose `stat-1m` because the backend typically only emits 5-minute and longer aggregate… **Function** ```typescript (opts: { includeOneMinute?: boolean } = {}) => TimelineOption[] ``` ### `normalizeAlertSeverity` Narrow an arbitrary backend severity string to the `AlertSeverity` literal union expected by the `ActiveIncidentsCard` row component. Unknown values fall back to `'medium'` so the row still renders rather than crashing on an unexpected pay… **Function** ```typescript (raw: string | null | undefined) => AlertSeverity ``` ### `readHashrateMhs` Reads the hashrate aggregate from a tail-log entry. Mining OS emits `hashrate_mhs_1m_sum_aggr` across every `stat-*` bucket, so a single field check covers all timelines; the `_5m_` legacy fallback is retained as a defensive secondary in c… **Function** ```typescript (entry: { hashrate_mhs_1m_sum_aggr?: unknown hashrate_mhs_5m_sum_aggr?: unknown }) => number | undefined ``` ### `SEVERITY_WEIGHT` Severity level → numeric weight. Higher is more urgent. Used for sorting the active-incidents list (most-severe first). **Constant** ```typescript Record ``` # General Utilities (/reference/ui/utilities/general) General-purpose helper functions. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ALERT_TYPE_POOL_NAME` **Constant** ```typescript { readonly ip_worker_name: "IP worker name"; readonly wrong_miner_pool: "Wrong miner pool"; readonly wrong_worker_name: "Wrong worker name"; readonly wrong_miner_subaccount: "Wrong miner subaccount";… ``` ### `ALERT_TYPE_POOL_VALUE` **Constant** ```typescript { readonly IP_WORKER_NAME: "ip_worker_name"; readonly WRONG_MINER_POOL: "wrong_miner_pool"; readonly WRONG_WORKER_NAME: "wrong_worker_name"; readonly WRONG_MINER_SUBACCOUNT: "wrong_miner_subaccount";… ``` ### `appendContainerToTag` **Function** ```typescript (deviceId: string) => string ``` ### `appendIdToTag` **Function** ```typescript (deviceId: string) => string ``` ### `appendIdToTags` **Function** ```typescript (deviceIdList: string[]) => string[] ``` ### `AUTH_LEVELS` **Constant** ```typescript { readonly READ: "r"; readonly WRITE: "w"; } ``` ### `AUTH_PERMISSIONS` **Constant** ```typescript { readonly TEMP: "temp"; readonly MINER: "miner"; readonly USERS: "users"; readonly ALERTS: "alerts"; readonly TICKETS: "tickets"; readonly ACTIONS: "actions"; readonly REVENUE: "revenue"; readonly F… ``` ### `AUTH_TOKEN_QUERY_PARAM` Pure URL helpers shared by the auth flow. Kept framework-agnostic so the react-adapter (and any future framework adapter) can call them without pulling in router-specific APIs. **Constant** ```typescript "authToken" ``` ### `buildCabinetDetailParams` List-things params for one LV cabinet's family of devices — the powermeters and temperature sensors whose `info.pos` sits under the cabinet `root`. Mirrors the reference app's `getLvCabinetDevicesByRoot(root)`; the detail hook groups the r… **Function** ```typescript (root: string) => ListThingsParams ``` ### `buildContainerCrossThing` `{ type: 'container', params: { containers } }` fan-out for miner-level actions. **Function** ```typescript (containers: string[]) => DeviceActionCrossThing ``` ### `buildContainerDetailParams` List-things params for the selected containers' detail snapshots. Takes the raw container keys (the `selectedDevicesTags` outer keys / `info.container` names), tags them with `container-` and filters to `t-container` things — mirrors the r… **Function** ```typescript (containerKeys: string[]) => ListThingsParams ``` ### `buildContainerWidgetsListParams` List-things params for the Site Overview container widgets grid. **Function** ```typescript () => ListThingsParams ``` ### `buildContainerWidgetsRealtimeTailLogParams` Tail-log params for the Container Widgets realtime snapshot — the latest `stat-realtime` sample across all miners, grouped so the cards can slice per container. **Function** ```typescript () => TailLogParams ``` ### `buildDeviceActionSubmission` Core submission assembler. Prefer the per-action builders below — they pin each action's param arity/encoding; this is the escape hatch for actions without a dedicated builder. `extras` is spread first so it can never override the pinned s… **Function** ```typescript (action: DeviceActionValue, tags: string[], params: VotingActionParam[] = [], extras: Record = {}) => DeviceActionSubmission ``` ### `buildExplorerListThingsParams` List-things params for one Explorer tab: tag-filtered, status-enriched, projected to `OP_CENTRE_LIST_THINGS_FIELDS`. **Function** ```typescript (tab: ExplorerTabValue, options: { limit?: number; offset?: number } = {}) => ListThingsParams ``` ### `buildMinerCrossThing` `{ type: 'miner', params: { containers } }` fan-out for container-level actions. **Function** ```typescript (containers: string[]) => DeviceActionCrossThing ``` ### `buildRebootAction` `reboot` — no params. **Function** ```typescript (tags: string[]) => DeviceActionSubmission ``` ### `buildResetAlarmAction` `resetAlarm` — no params. **Function** ```typescript (tags: string[]) => DeviceActionSubmission ``` ### `buildSetAirExhaustEnabledAction` `setAirExhaustEnabled` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSetLedAction` `setLED` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSetPlcRegistersAction` `setPlcRegisters` (Gamma) — single `{ register: value }` map param. **Function** ```typescript (tags: string[], registers: Record) => DeviceActionSubmission ``` ### `buildSetPowerModeAction` `setPowerMode` — single power-mode param, optional container fan-out. **Function** ```typescript (tags: string[], mode: PowerModeValue, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSetPowerPctAction` `setPowerPct` — percentage encoded as a string, optional container fan-out. **Function** ```typescript (tags: string[], percentage: number, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSetTankEnabledAction` `setTankEnabled` — positional `[tankNumber, isOn]`. **Function** ```typescript (tags: string[], tankNumber: number, isOn: boolean) => DeviceActionSubmission ``` ### `buildSwitchContainerAction` `switchContainer` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSwitchCoolingSystemAction` `switchCoolingSystem` — single boolean param, optional miner fan-out. **Function** ```typescript (tags: string[], isOn: boolean, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSwitchSocketAction` `switchSocket` — one `{ pdu, socket, enabled }` param per toggled socket. **Function** ```typescript (tags: string[], sockets: SocketSwitch[]) => DeviceActionSubmission ``` ### `buildUpdateThingBatchEntry` One `updateThing` entry inside a move/add/replace-miner batch — carries the miner's new rack/position/network config plus the owning `minerId` the backend uses to group batch progress. **Function** ```typescript (params: UpdateThingParams, minerId: string = params.id) => VotingActionPayload ``` ### `CABINET_DEVICES_TYPES_NAME_MAP` **Constant** ```typescript { readonly "powermeter-abb-b24": "Powermeter ABB B24"; readonly "sensor-temp-seneca": "Sensor Temp Seneca"; readonly "powermeter-abb-m4m20": "Powermeter ABB M4M20"; readonly "powermeter-abb-m1m20": "… ``` ### `checkPermission` Check if user has the requested permission **Function** ```typescript (config: AuthConfig | null | undefined, { perm, write, cap }: PermissionCheck) => boolean ``` ### `COMPLETE_CONTAINER_TYPE` **Constant** ```typescript { readonly BITMAIN_HYDRO: "container-as-hk3"; readonly BITDEER_M30: "container-bd-d40-m30"; readonly BITDEER_M56: "container-bd-d40-m56"; readonly MICROBT_ALPHA: "container-mbt-alpha"; readonly BITDE… ``` ### `COMPLETE_MINER_TYPES` **Constant** ```typescript { readonly ANTMINER_AM_S21: "miner-am-s21"; readonly WHATSMINER_WM_63: "miner-wm-m63"; readonly WHATSMINER_WM_53: "miner-wm-m53s"; readonly AVALON_AV_a1346: "miner-av-a1346"; readonly WHATSMINER_WM_5… ``` ### `CONTAINER_LIST_THINGS_LIMIT` **Constant** ```typescript 1000 ``` ### `CONTAINER_MODEL` Container tag / type / threshold literals. Data-layer contracts shared across the toolkit — owned by ui-foundation so the React layers stay free of tag strings. **Constant** ```typescript { readonly M221: "m221"; readonly GAMMA: "gamma"; readonly BITDEER: "bitdeer"; readonly BITMAIN: "bitmain"; readonly MICROBT: "microbt"; readonly ANTSPACE: "antspace"; readonly BITMAIN_IMM: "bitmain-… ``` ### `CONTAINER_MODEL_FAMILY` Container model families the tab matrix distinguishes. Detection order matters and mirrors the reference app's if/else chain — see `resolveContainerModelFamily`. **Constant** ```typescript { readonly BITDEER: "bitdeer"; readonly ANTSPACE_HYDRO: "antspace-hydro"; readonly ANTSPACE_IMMERSION: "antspace-immersion"; readonly MICROBT: "microbt"; readonly GAMMA: "gamma"; } ``` ### `CONTAINER_SETTINGS_MODEL` **Constant** ```typescript { BITDEER: string; MICROBT: string; HYDRO: string; IMMERSION: string; } ``` ### `CONTAINER_STATUS` **Constant** ```typescript { readonly RUNNING: "running"; readonly OFFLINE: "offline"; readonly STOPPED: "stopped"; } ``` ### `CONTAINER_TAB` Container detail-view tab keys. The per-model availability matrix lives in `utils/container-tabs.ts`. **Constant** ```typescript { readonly PDU: "pdu"; readonly HOME: "home"; readonly ALARM: "alarm"; readonly CHARTS: "charts"; readonly HEATMAP: "heatmap"; readonly CONTROLS: "controls"; readonly SETTINGS: "settings"; readonly P… ``` ### `CONTAINER_TAB_LABEL` Display labels for the container detail tabs, mirroring the reference app's `containerTabsHelper` (`PDU` renders as "PDU Layout", the rest are capitalised keys). **Constant** ```typescript { readonly pdu: "PDU Layout"; readonly home: "Home"; readonly alarm: "Alarm"; readonly charts: "Charts"; readonly heatmap: "Heatmap"; readonly controls: "Controls"; readonly settings: "Settings"; rea… ``` ### `CONTAINER_TAB_MATRIX` Base tab sequence per model family — order is display order. Power Adjustment is intentionally absent here: it is inserted positionally by `getSupportedContainerTabs`. **Constant** ```typescript Record ``` ### `CONTAINER_TACTICS_TYPE` **Constant** ```typescript { readonly COIN: "coin"; readonly DISABLED: "disabled"; readonly ELECTRICITY: "electricity"; } ``` ### `CONTAINER_TYPE` **Constant** ```typescript { readonly BITDEER: "bd"; readonly ANTSPACE: "as"; readonly MICROBT: "mbt"; readonly ANTSPACE_HYDRO: "as-hk3"; readonly ANTSPACE_IMMERSION: "as-immersion"; } ``` ### `CONTAINER_TYPE_NAME_MAP` **Constant** ```typescript { readonly "container-bd-d40-m30": "Bitdeer M30"; readonly "container-bd-d40-m56": "Bitdeer M56"; readonly "container-bd-d40-s19xp": "Bitdeer S19XP"; readonly "container-as-hk3": "Bitmain Hydro"; rea… ``` ### `CONTAINER_WIDGETS_AGGR_FIELD_KEYS` All realtime aggregate fields the cards read — the tail-log `aggrFields` set. **Constant** ```typescript readonly string[] ``` ### `CONTAINER_WIDGETS_SUMMARY_FIELD` Realtime summary aggregate field names (these carry the `_aggr` suffix). **Constant** ```typescript { readonly HASHRATE_MHS_1M_SUM: "hashrate_mhs_1m_group_sum_aggr"; readonly TEMPERATURE_MAX: "temperature_c_group_max_aggr"; readonly TEMPERATURE_AVG: "temperature_c_group_avg_aggr"; } ``` ### `CONTAINERS_MINER_TYPE` **Constant** ```typescript { readonly M56: "m56"; readonly M30: "m30"; readonly A1346: "a1346"; readonly S19XP: "s19xp"; } ``` ### `DEFAULT_HISTORICAL_WINDOW_MS` Default historical-alerts look-back window (14 days), matching the devkit `<Alerts>` feature default. Wider ranges fan out into more 24h requests — see `fetchHistoricalAlertsInChunks`. **Constant** ```typescript number ``` ### `deriveContainerActivity` Slice the realtime aggregate into one container's per-status miner counts. `total` is the container's nominal miner capacity; miners not accounted for by any status count collapse into `disconnected` (never negative). With no realtime samp… **Function** ```typescript (realtime: TailLogEntry | undefined, containerModel: string, total: number) => ContainerActivity ``` ### `deriveContainerSummary` **Function** ```typescript (realtime: TailLogEntry | undefined, containerModel: string) => ContainerSummary ``` ### `deriveContainerTanks` Derive the per-tank readings for a container's immersion cooling system — one entry per oil pump, joined with the matching water pump and (when present) the tank pressure. Returns an empty array for containers without an immersion cooling… **Function** ```typescript (container: ListThingsDevice) => TankReading[] ``` ### `deriveSelectedSockets` Derive the store's `selectedSockets` map (keyed by container tag) from the selected device-tags — the pure body of the reference app's `findAndSetSelectedSockets`. Each per-container tag key (`pos-…` / `id-…`) is stripped of its `pos-` pre… **Function** ```typescript (containers: ContainerSnapshotForSockets[] | undefined, selectedDevicesTags: Record>, allDevices: MinerForSocket[] | undefined) => Record ``` ### `exportSettingsToFile` **Function** ```typescript (data: SettingsExportData) => string ``` ### `filterUsers` **Function** ```typescript ({ users, email, role }: FilterUsersParams) => SettingsUser[] ``` ### `findMatchingContainer` Find the container-settings row for a container type: an exact `model` match first, else the settings-model family fallback. Mirrors the reference app's `findMatchingContainer`. **Function** ```typescript (settings: ContainerSettingsEntry[] | undefined, containerType: string | undefined) => ContainerSettingsEntry | null ``` ### `flattenKernelEnvelope` Flattens the per-Kernel response envelope the gateway wraps around merged worker responses (`/auth/list-things`, `/auth/list-racks`, ... return `[[row, ...], [row, ...]]` — one inner array per Kernel). Null-safe on both levels: a missing e… **Function** ```typescript (envelope: ReadonlyArray | null | undefined) => T[] ``` ### `formatLastActive` **Function** ```typescript (timestamp: string | undefined) => string ``` ### `formatRoleLabel` **Function** ```typescript (role: string) => string ``` ### `getAntspaceHydroIndexes` Antspace Hydro position → `[rack, pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getAntspaceImmersionIndexes` Antspace Immersion position → `[pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getBitdeerIndexes` Bitdeer / MicroBT position → `[pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getByIdsQuery` **Function** ```typescript (ids: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsWithAlertsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsWithCriticalAlertsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByThingsAttributeQuery` **Function** ```typescript (filterAttributes: FilterAttribute[], selectedTypes: string[], allowEmptyArray?: boolean) => UnknownRecord ``` ### `getByTypesQuery` **Function** ```typescript (filterTypes: string[], allowEmptyArray?: boolean) => string ``` ### `getConnectedMinerForSocket` The selected miner sitting at `pos`, if any (miners only, exact position). **Function** ```typescript (devices: MinerForSocket[] | undefined, pos: string) => MinerForSocket | undefined ``` ### `getContainerByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getContainerMinersByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getContainerSettingsModel` Map a container type string to its settings-model key (`bd` / `mbt` / `hydro` / `immersion`), or `null` for an unknown family. Mirrors the reference app's `getContainerSettingsModel`. **Function** ```typescript (containerType: string | undefined) => string | null ``` ### `getDeviceByAlertId` **Function** ```typescript (uuid: string) => string ``` ### `getFiltersQuery` **Function** ```typescript (filterTags?: string[], filters?: Record, selectedTypes: string[] = ['t-container']) => UnknownRecord ``` ### `getListQuery` **Function** ```typescript (filterTags: string[], filters?: Record, selectedTypes: string[] = ['t-container']) => string ``` ### `getLvCabinetDevicesByRoot` **Function** ```typescript (root: string) => string ``` ### `getMinersByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getPduByIndex` The PDU row whose `pdu` matches `pduIndex` on this container, or undefined. **Function** ```typescript (container: ContainerSnapshotForSockets | undefined, pduIndex: string | number) => PduSnapshot | undefined ``` ### `getPduData` The `pdu_data` array off a container detail snapshot, or undefined. **Function** ```typescript (last: ContainerSnapshotForSockets['last']) => PduSnapshot[] | undefined ``` ### `getRolesFromAuthToken` Extract roles from authentication token **Function** ```typescript (authToken?: string) => string[] ``` ### `getSignInRedirectUrl` Get redirect URL based on user's primary role **Function** ```typescript (authToken: string | null | undefined) => string ``` ### `getSitePowerMeterQuery` **Function** ```typescript () => string ``` ### `getSocketInfo` Resolve one socket for `pos` on `container`: pick the vendor index scheme off the container name, then (for Bitdeer / MicroBT) join the live PDU row so `enabled` / `cooling` reflect the snapshot. Hydro / Immersion have no live socket table… **Function** ```typescript (container: ContainerSnapshotForSockets, pos: string, allDevices: MinerForSocket[] | undefined) => DerivedSocket ``` ### `getSupportedContainerTabs` Full tab sequence for a container type: the family's base sequence, plus Power Adjustment spliced in after the PDU tab for Whatsminer containers. Unknown types resolve to an empty list. **Function** ```typescript (type: string | undefined) => ContainerTabValue[] ``` ### `getWidgetAlarmState` Resolve a container's alarm state from its live stats and matched settings. **Function** ```typescript (_container: ListThingsDevice, _settings: ContainerSettingsEntry | null = null) => ContainerAlarmState ``` ### `isAntminer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAntspaceHydroContainer` Antspace/Bitmain hydro family (`as-hk3`, `antspace-hydro`, `bitmain-hydro`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAntspaceImmersionContainer` Antspace/Bitmain immersion family (`as-immersion`, `bitmain-imm[ersion]`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAvalon` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isBitdeerContainer` Bitdeer family — `container-bd-*` and anything mentioning `bitdeer`. **Function** ```typescript (type: string | undefined) => boolean ``` ### `isContainer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isGammaContainer` Gamma family (`m221`, `gamma`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isMicroBTContainer` MicroBT family (`mbt`, `microbt`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isMiner` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isPduContainerTab` The PDU grid renders under the `pdu` tab key. **Function** ```typescript (tab: string | undefined) => boolean ``` ### `isWhatsminer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isWhatsminerContainer` Containers populated with Whatsminer miners (`m56` / `m30` positions, or any MicroBT container) — the set that gets the Power Adjustment tab. **Function** ```typescript (type: string | undefined) => boolean ``` ### `LV_CABINET_DEVICES_TAG` **Constant** ```typescript { readonly POWERMETER: "t-powermeter"; readonly SENSOR_TEMP: "t-sensor-temp"; } ``` ### `LV_CABINET_DEVICES_TYPE` **Constant** ```typescript { readonly POWERMETER_ABB_B24: "powermeter-abb-b24"; readonly SENSOR_TEMP_SENECA: "sensor-temp-seneca"; readonly POWERMETER_ABB_M1M20: "powermeter-abb-m1m20"; readonly POWERMETER_ABB_M4M20: "powermet… ``` ### `MAINTENANCE_CONTAINER` **Constant** ```typescript "maintenance" ``` ### `MINER_BRAND_NAMES` **Constant** ```typescript { readonly av: "Avalon"; readonly am: "Antminer"; readonly wm: "Whatsminer"; } ``` ### `MINER_MODEL` Device / miner / power-meter tag + type literals. These are data-layer contracts shared between the API surface and the UI — owned by ui-foundation so the React layers can stay free of tag strings. **Constant** ```typescript { readonly AVALON: "avalon"; readonly ANTMINER: "antminer"; readonly WHATSMINER: "whatsminer"; } ``` ### `MINER_MODEL_TO_TYPE_MAP` **Constant** ```typescript { readonly av: "avalon"; readonly am: "antminer"; readonly wm: "whatsminer"; } ``` ### `MINER_POWER_MODE` **Constant** ```typescript { readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readonly HIGH: "high"; } ``` ### `MINER_TYPE` **Constant** ```typescript { readonly AVALON: "av"; readonly ANTMINER: "am"; readonly WHATSMINER: "wm"; } ``` ### `MINER_TYPE_MESSAGE` **Constant** ```typescript { readonly "miner-av-a1346": "A1346 miners do not report consumption individually, so Avg. efficiency cannot be calculated"; } ``` ### `MINER_TYPE_NAME_MAP` **Constant** ```typescript { readonly "miner-av-a1346": "Avalon A1346"; readonly "miner-am-s21": "Antminer S21"; readonly "miner-wm-m63": "Whatsminer M63"; readonly "miner-wm-m56s": "Whatsminer M56S"; readonly "miner-wm-m53s":… ``` ### `MinerStatuses` **Constant** ```typescript { readonly MINING: "mining"; readonly OFFLINE: "offline"; readonly SLEEPING: "sleeping"; readonly ERROR: "error"; readonly NOT_MINING: "not_mining"; readonly MAINTENANCE: "maintenance"; readonly ALER… ``` ### `NO_MAINTENANCE_CONTAINER` **Constant** ```typescript "no_maintenance" ``` ### `ONE_DAY_MS` One day in milliseconds — the historical-log fetch window size. **Constant** ```typescript number ``` ### `OP_CENTRE_CABINET_DETAIL_FIELDS` The cabinet-detail field projection — the sensor/powermeter fields the LV cabinet detail reads: each device's `power_w` / `temp_c` reading, status (for the offline marker) and `last.alerts` (for the warnings timeline). **Constant** ```typescript string ``` ### `OP_CENTRE_CONTAINER_DETAIL_FIELDS` The container-detail field projection — a superset of the list projection that also pulls the full `last.snap.stats` (so the socket transform sees `container_specific.pdu_data`, ambient temp, humidity, …) and the full `last.snap.config` (p… **Constant** ```typescript string ``` ### `OP_CENTRE_CONTAINER_WIDGETS_FIELDS` Container list projection for the Site Overview widgets — the lean list fields plus `last.snap.stats.container_specific` (cooling system: oil / water pumps, tanks) and `last.snap.config` (per-vendor thresholds), which the vendor boxes (e.g… **Constant** ```typescript string ``` ### `OP_CENTRE_LIST_THINGS_FIELDS` The list-things field projection the Explorer tables and Container Widgets cards read — the reference app's `LIST_THINGS_FIELDS`, kept as one projection so every consumer sees the same row shape. **Constant** ```typescript string ``` ### `parseSettingsFile` **Function** ```typescript (file: File) => Promise ``` ### `PM_ATTRIBUTE_LABEL_MAP` **Constant** ```typescript { readonly 'I1 a': "Current L1"; readonly 'I2 a': "Current L2"; readonly 'I3 a': "Current L3"; readonly 'V3 n v': "Voltage L3-N"; readonly 'V2 n v': "Voltage L2-N"; readonly 'V1 n v': "Voltage L1-N";… ``` ### `POWER_MODE` Miner power modes accepted by `setPowerMode`. **Constant** ```typescript { readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readonly HIGH: "high"; } ``` ### `removeContainerPrefix` **Function** ```typescript (text: string) => string ``` ### `resolveContainerModelFamily` Resolves a raw container `type` string (e.g. `container-bd-d40-m56`) to its model family, or `undefined` for unknown types. First match wins, in the reference app's original branch order. **Function** ```typescript (type: string | undefined) => ContainerModelFamily | undefined ``` ### `SITE_OVERVIEW_STATUSES` **Constant** ```typescript { readonly OFFLINE: "offline"; readonly EMPTY: "empty"; readonly NOT_MINING: "not_mining"; readonly MINING: "mining"; } ``` ### `SOCKET_STATUSES` **Constant** ```typescript { readonly ERROR_MINING: "errorMining"; readonly MINER_DISCONNECTED: "disconnected"; readonly CONNECTING: "connecting"; readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readon… ``` ### `THRESHOLD_LEVEL` **Constant** ```typescript { readonly ALERT: "alert"; readonly ALARM: "alarm"; readonly NORMAL: "normal"; readonly ALARM_LOW: "alarmLow"; readonly ALARM_HIGH: "alarmHigh"; readonly CRITICAL_LOW: "criticalLow"; readonly CRITICA… ``` ### `THRESHOLD_TYPE` **Constant** ```typescript { readonly TANK_PRESSURE: "tankPressure"; readonly OIL_TEMPERATURE: "oilTemperature"; readonly WATER_TEMPERATURE: "waterTemperature"; readonly SUPPLY_LIQUID_PRESSURE: "supplyLiquidPressure"; } ``` ### `USER_ROLE` **Constant** ```typescript { readonly ADMIN: "admin"; readonly READ_ONLY: "read_only_user"; readonly SITE_MANAGER: "site_manager"; readonly SITE_OPERATOR: "site_operator"; readonly FIELD_OPERATOR: "field_operator"; readonly RE… ``` ### `validateSettingsJson` **Function** ```typescript (data: unknown) => data is SettingsExportData ``` ### `VOTING_SUBMISSION_TYPE` Submission `type` for the voting/approval workflow. **Constant** ```typescript "voting" ``` # Core Utils (/reference/ui/utilities/utils) Core utility functions for formatting, validation, and data processing. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `getLatestSample` Tiny projection helper used by the dashboard's header-stat hooks. **Function** ```typescript (entries: readonly T[] | null | undefined) => T | undefined ``` # Code of Conduct (/support/community/code-of-conduct) ## Our Commitment MDK is committed to fostering an open, professional, and respectful community. We welcome contributors of all backgrounds and experience levels. Participation in the MDK community should be harassment-free and inclusive for everyone. --- ## Expected Behavior All participants in the MDK community are expected to: - Be respectful and constructive in communication - Provide helpful and professional feedback - Assume good intent - Focus on what is best for the project - Accept constructive criticism gracefully --- ## Unacceptable Behavior The following behaviors are not tolerated: - Harassment, discrimination, or hateful conduct - Personal attacks or insulting language - Public or private harassment - Trolling, intimidation, or deliberate disruption - Publishing private information without consent - Any conduct that would be considered unprofessional in a workplace setting --- ## Scope This Code of Conduct applies to: - GitHub repositories - Issues and pull requests - Discussions and community channels - Official MDK communication platforms - Any other space officially associated with the MDK project --- ## Enforcement The Community Manager is responsible for enforcing this Code of Conduct. **Community Manager:** Gio\ **Lead Maintainer:** Hemant T If you experience or witness unacceptable behavior, report it privately to the Community Manager. Reports will be handled confidentially and reviewed in coordination with the MDK core team. --- ## Enforcement Guidelines The MDK core team may take any action deemed appropriate, including: - Warning the participant - Temporarily restricting access - Permanently banning a participant from the community - Removing content that violates this Code of Conduct Decisions regarding enforcement are final. --- ## Amendments This Code of Conduct may be updated from time to time by the MDK core team to reflect evolving community needs. # Contributing (/support/community/contributing) # Contribute to MDK Thank you for your interest in contributing to [MDK](https://github.com/tetherto/mdk/). This document outlines the contribution workflow for the MDK repository, from setting up your development environment to submitting pull requests and participating in releases. ## Security If you discover a security vulnerability, do not report it in a public issue. Please follow the private disclosure instructions in [SECURITY.md](https://github.com/tetherto/mdk/blob/main/SECURITY.md). ## Monorepo structure MDK is a monorepo with separate backend and frontend workspaces: - Backend: - `backend/core/`: Backend services, container modules, and integration/unit tests (npm-based) - `backend/workers/`: Protocol-translator worker packages (miners, miner-pools, power-meter, temperature, containers), per-worker mock servers, and per-worker tests (npm-based) - Frontend: `ui/`: Frontend packages, demo app, and shared UI foundation (npm + Turbo-based) Choose the backend or frontend workflow that matches the area you are contributing to. ### Root configuration must be domain-aware The repo top level is a fixed set of domains (`ui/`, `backend/`, `docs/`, `examples/`) plus tooling and repo-meta files. Shared root config (today just `.gitignore`) is read across all of them, so every pattern must be written so it cannot silently match another domain's source: - **Anchor anything that targets one domain's build or runtime output.** Use `/name/` for the repo root or `domain/**/name/` for a subtree. A bare `status` / `store` / `tmp` / `Checklist*` matches a file or directory of that name *anywhere*, including UI source. That is exactly what caused a prior root ignore regression, where bare `status` / `store` swallowed `ui/packages/ui-foundation/src/store/`. - **Keep per-domain ignores in that domain's own `.gitignore`** (`ui/.gitignore`, the per-package backend `.gitignore`s), not the root. Things like `dist`, `.turbo`, and `build` belong to a domain. - **Lint/format/type config is domain-owned, not shared at the root.** `ui/` ships its own `eslint.config.mjs` / `tsconfig.base.json` / `.prettierrc`; backend uses `standard`. Do not add a root-level eslint/tsconfig/prettier that would apply across domains. - **A genuinely shared convention is fine if it applies identically to every domain** - e.g. committing `config/*.json.example` while ignoring the generated `config/*.json`. Note it as shared so the intent is clear. ## Get started ### Prerequisites Before contributing, ensure you have the following installed: - **Node.js** (version >=24) - **Git** (latest stable version) - **npm** (version 11 or higher) ### Licensing MDK is released under the [**Apache License 2.0**](https://github.com/tetherto/mdk/blob/main/LICENSE). By contributing, you agree that: - You retain copyright over your contributions - You grant a perpetual, worldwide, royalty-free license for their use - Contributions are provided **“AS IS”**, without warranty ## Development environment setup
1. Fork and clone 1. Fork [the repository](https://github.com/tetherto/mdk.git) on GitHub. 2. Clone your fork locally and navigate into the project directory: ```bash git clone https://github.com/username/mdk.git cd mdk ``` 1. Add the upstream remote: ```bash git remote add upstream https://github.com/tetherto/mdk.git ```
2. Stay in sync Keep your fork in sync with the main repository. For example: ```bash git fetch upstream git merge --ff-only upstream/main # fails loudly if main has diverged ```
### Backend contribution setup Use this workflow when contributing to backend code under `backend/core/`. ```bash cd backend/core npm install ``` #### Common commands ```bash # Lint backend code npm run lint # Run backend test suite (lint + unit + integration + package tests) npm test ``` ### Frontend contribution setup Use this workflow when contributing to frontend code under `ui/`. ```bash cd ui npm install ``` #### Common commands ```bash # Build packages npm run build # Run dev mode (all packages + demo) npm run dev # Lint and type-check npm run lint npm run typecheck # Run tests npm test ``` ## Pull request workflow ### Conventional types MDK uses Conventional Commits-style types for both branch names and PR titles. | Type | Use for | |---|---| | `feat` | New features | | `fix` | Bug fixes | | `docs` | Documentation changes | | `refactor` | Code refactoring without behaviour change | | `test` | Test additions or changes | | `chore` | Tooling, dependencies, repo maintenance | | `perf` | Performance improvements | | `style` | Formatting only (no logic change) | | `ci` | CI configuration changes | | `build` | Build system or external dependency changes | ### Branch naming convention Create branches using the following pattern: ```bash {type}/{short-description} ``` Where `{type}` is one of the [conventional types](#conventional-types). #### Branch naming examples ```bash # New feature git checkout -b feat/mdk-new-device # Bug fix git checkout -b fix/timeout-handling ``` ### Commit message template The repository ships a commit message template at `.gitmessage` that pre-fills the commit editor with the expected format (type, summary, body, and Asana/Related PR references). Enable it once per clone: ```bash git config commit.template .gitmessage ``` This is a local convenience only — it is not enforced, and the [conventional types](#conventional-types) above remain the source of truth for commit, branch, and PR naming. ### Pull request steps 1. Sync your local main with upstream `main`. 2. Create a branch from local `main`. 3. Make your code changes. 4. Write or update tests. 5. Run linting and tests locally in the workspaces you changed: - `core`: `npm run lint && npm test` - `ui`: `npm run lint && npm test` (and `npm run typecheck` for TypeScript changes) 6. Commit changes with meaningful messages. 7. Push your branch and open a Pull Request targeting the upstream `main`. ### PR checklist Before submitting your PR, ensure that: - [ ] Code builds locally (`npm run build` for `ui` changes) - [ ] Tests pass in affected workspaces (`npm test`) - [ ] Linting passes (`npm run lint`) - [ ] Type-check passes for frontend TypeScript changes (`npm run typecheck`) - [ ] New features include tests - [ ] Public behavior or APIs changes have a [`docs-needed` issue](https://github.com/tetherto/mdk/issues/new?template=docs-needed.yml) linked to the PR ### PR title format Use the following convention: ```bash {type}({scope}): {description} ``` Where `{type}` is one of the [conventional types](#conventional-types) and `{scope}` is the affected area, for example `miner` or `ui`. Examples: - `feat(miner): add Antminer S21 support` - `fix(timeout): resolve action timeout handling` - `docs(api): update stats documentation` ## PR review All pull requests go through the following review steps: 1. **Automated checks**: Linting and tests must pass. 2. **Code review**: At least 2 maintainer approvals are required. 3. **Feedback resolution**: All requested changes must be addressed. 4. **Squash and merge**: Maintainers squash commits to keep history clean. ### Workflow diagram ```mermaid flowchart TB subgraph contributor [Contributor] start((Start)) --> createBranch[Create branch from main] createBranch --> test[Run tests] test --> testGw{Tests pass?} testGw -->|No| fix[Fix issues] fix --> test testGw -->|Yes| createPR[Create PR] createPR --> review address[Address feedback] --> pushFixes[Push fixes] pushFixes --> test end subgraph reviewer [Reviewer / Maintainer] review[Code review] reviewGw{Approved?} review --> reviewGw reviewGw -->|Request changes| address reviewGw -->|Rejected| cancel[Close PR] reviewGw -->|Approved| merge[Merge to main] merge --> tagGw{Tag release?} tagGw -->|No| endNoTag((End)) tagGw -->|Yes| tag[Tag version] tag --> deploy[Deploy] deploy --> deployGw{Deploy success?} deployGw -->|Yes| endSuccess((End)) deployGw -->|No| rollback[Rollback] rollback --> fix end ``` ## Code standards MDK uses **StandardJS** style to keep the codebase consistent and easy to review across repositories. Key rules: - 2-space indentation - No semicolons - Single quotes for strings - Space after keywords (`if`, `for`, `while`) - No unused variables ## Versioning and tagging MDK follows **Semantic Versioning**: - **MAJOR** (`1.x.x`): breaking changes - **MINOR** (`x.1.x`): new backward-compatible features - **PATCH** (`x.x.1`): backward-compatible bug fixes When the MDK team provides a release, they are cut from a `release/` branch, verified in CI, promoted to the public repo, then tagged. For the full release process and checklist, see RELEASING.md. Happy contributing, and thanks for helping improve MDK! 🚀 # Governance (/support/community/governance) This document describes how the MDK project is governed and how decisions are made. MDK is an open-source project. While the code is publicly available and community contributions are welcome, final decision-making authority rests with the MDK core team. --- ## Project Roles ### Users Anyone who uses MDK and provides feedback, bug reports, or feature requests. ### Contributors Community members who contribute code, documentation, tests, or other improvements via pull requests or issues. Contributors do not have merge access. --- ### Maintainers Maintainers are responsible for reviewing pull requests, maintaining code quality, and ensuring alignment with the project roadmap. Maintainers may be appointed by the Lead Maintainer. The following GitHub IDs currently hold maintainer status for MDK: - arif-dewi - eugeneglova - robdll - eskawl - boris91 - habrahamyanbf - efr-nox - rob-aslanian - mukama - paragmore - tekwani Only the GitHub accounts listed above have maintainer privileges within the MDK repositories. Maintainer status is granted based on demonstrated technical expertise, sustained contribution, and alignment with project goals. --- ### MDK core team The MDK Core Team consists of all active developers, the Project Manager, and the Community Manager. The Core Team is responsible for the overall health, sustainability, and long-term success of the project. While specific authorities are defined for the Lead Maintainer (technical) and the Community Manager (strategic), the Core Team operates as the primary collaborative decision-making body. ### Lead Maintainer **Hemant T** is the Lead Maintainer and Technical Lead for MDK. The Lead Maintainer has final authority over: - Pull request approval and merging - Architecture and design decisions - Release planning and versioning - Accepting or rejecting features - Appointing or removing maintainers If consensus cannot be reached among maintainers, the Lead Maintainer makes the final decision. --- ### Community Manager **Gio** is the Community Manager for MDK. The Community Manager is responsible for: - Managing community communication channels - Moderating discussions and enforcing the Code of Conduct - Supporting contributors during onboarding - Acting as a bridge between the community and the MDK core team - Defining, maintaining, and communicating the project vision and long-term direction - Leading roadmap prioritization and strategic planning --- ## Decision-Making Process - Community members may propose changes via issues or pull requests - Maintainers review contributions for quality, security, and alignment with MDK goals - The Lead Maintainer has final approval authority on all technical decisions - Strategic, roadmap, or breaking changes are determined by the MDK core team - The Community Manager has final decision-making authority over all strategic and directional matters, including project vision, roadmap prioritization, major feature introductions, partnerships, and significant pivots - In cases where strategic direction and technical considerations intersect, the Community Manager determines the final strategic outcome, while the Lead Maintainer determines the final technical implementation approach. --- ## Contribution Review Process - All contributions must follow `CONTRIBUTING.md` - Pull requests require review by a maintainer - Final approval and merge is performed by the Lead Maintainer - The MDK team reserves the right to decline contributions that do not align with the project direction --- ## Inactivity and Removal Maintainers who become inactive for an extended period or violate project policies may be removed by the Lead Maintainer. --- ## Code of Conduct All participants are expected to follow the project's Code of Conduct. Violations are handled by the Community Manager in coordination with the MDK core team. See [Code of Conduct](/support/community/code-of-conduct) for details. --- ## Changes to Governance This governance model may evolve over time. Any changes will be proposed and approved by the MDK core team. # MDK Repositories (/support/resources/repositories) The MDK monorepo makes the frontend and backend components publicly accessible: - [https://github.com/tetherto/mdk](https://github.com/tetherto/mdk) *MDK is developed by Tether and released under the [Apache 2.0 license](/support/community/contributing#licensing).* # Roadmap (/support/resources/roadmap) MDK follows a **two-week release cadence** to keep progress visible, collect feedback early, and progressively harden the platform — from a rudimentary end-to-end foundation toward a production-ready release. ## Release principles - **Release every two weeks** to keep momentum and feedback loops short - **Use four maturity phases** to mark clear readiness jumps - **Start with a working end-to-end developer experience**, then harden progressively - **Reach production readiness progressively**, not by a single large release - **Follow standard [semantic versioning](https://semver.org)** — the `0.x` line means MDK is still in development and not intended for production ## Versioning and naming strategy MDK uses standard [semantic versioning](https://semver.org) (`MAJOR.MINOR.PATCH`): - A **new version ships every two weeks**, bumping the **minor**: `0.2.0` → `0.3.0` → `0.4.0` → … - **Patch** releases (`0.x.1`) go out only when a fix is needed between the scheduled releases, e.g. 0.2.1 - While MDK is in the **`0.x` line**, interfaces may change and the platform is **not intended for production** — this is the standard semver signal, and we use it deliberately so developers can read it literally - **`1.0.0`** marks the first **production-ready** release Maturity is tracked by **phase**, not by version number — each phase spans several bi-weekly releases. The four phases describe how ready MDK is at each stage: | Phase | Production-readiness | |---|---| | **Foundation** | Experimental. End-to-end but rudimentary. Not stable. | | **Lab testing** | Complete enough to build against end-to-end, in a lab. Not stable, not for production. | | **On site testing** | Stable enough to test at real sites under real operating conditions, closely monitored. Not yet production-ready. | | **Production Ready** | Ready for production deployment, with stable interfaces and compatibility guarantees. | ## 2026 2026 moves MDK through **four phases**: - **May:** [Foundation](#foundation) - **July:** [Lab testing](#lab-testing) - **October:** [On site testing](#on-site-testing) - **December:** [Production Ready](#production-ready) Between phases, MDK ships a new release **every two weeks**, starting from the Foundation release at the end of May. The diagram below shows only the phase milestones; the bi-weekly releases land in the windows between them. ```mermaid graph TB subgraph foundation [Foundation] p1([May 2026

Foundation
Public, end-to-end but rudimentary]) it1[[Jun–Jul 2026
New release every 2 weeks]] end subgraph lab [Lab testing] p2([Jul 2026
Lab testing
Complete end-to-end, lab only]) it2[[Aug–Oct 2026
New release every 2 weeks]] end subgraph onsite [On site testing] p3([Oct 2026
On site testing
Testing at real sites]) it3[[Nov–Dec 2026
New release every 2 weeks]] end subgraph prod [Production Ready] p4([Dec 2026
Production Ready
Production-ready baseline]) end p1 --> it1 --> p2 --> it2 --> p3 --> it3 --> p4 style foundation fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style lab fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style onsite fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style prod fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` ## Release plan Phase descriptions below focus on the **level of maturity and production-readiness** at each stage. The exact feature scope of each two-week release is decided as we go and is not fixed by this roadmap. ### Foundation *Released end of May 2026.* The first public release is about **visibility, direction, and feedback**. It is intentionally rudimentary: developers can clone it, run an end-to-end example, and get a concrete feel for how MDK fits together. Interfaces are expected to change. The goal is to show where MDK is heading and invite feedback from developers, partners, and early contributors — not to support real workloads. ### Lab testing *Target: end of July 2026.* At this stage MDK is **complete enough to build against end to end**. Developers can run a full workflow and experiment with MDK in their own labs. It is **not stable** — breaking changes are still expected between releases — and it is **not ready for production**. The goal is to validate the end-to-end developer workflow and surface integration gaps before MDK is exposed to real operational conditions. ### On site testing *Target: end of October 2026.* This is the first stage intended for **testing at real sites**. MDK should be stable enough to run on site under real operating conditions, while still being monitored closely. The goal is to validate performance, robustness, deployment workflows, and operational fit in real scenarios. This is not yet a stability commitment — interfaces may still change before production readiness. ### Production Ready *Target: end of December 2026.* This is the first **production-ready** release (`1.0.0`). By this point MDK offers a solid baseline for production deployment, with stable core interfaces, validated workflows, and documentation that supports adoption by operators, integrators, and developers building on top of the platform. From here, standard semver compatibility guarantees apply. # Tutorials (/tutorials) ## New to MDK? Go from stack to dashboard Three short tutorials take you from a running MDK stack to a browser dashboard demo. First you watch the stack run, then you drive it from the CLI, then you launch the demo dashboard on top. Climb straight through, or jump to the rung you need. } className="@max-lg:col-span-1" title={1. Run the stack} href="/tutorials/backend-stack/run" description={ Observe — one command brings up Kernel with a mock device registered } /> } className="@max-lg:col-span-1" title={2. Control from the CLI} href="/tutorials/backend-stack/cli" description={ Interact — drive a running stack from a REPL over IPC } /> } className="@max-lg:col-span-1" title={3. Run a dashboard demo} href="/tutorials/full-stack/dashboard" description={ Demo — run a React dashboard with live charts on the stack } /> ## Get started with your framework } title={React} href="/tutorials/ui/react" description={ Pick a path: lean Quickstart, full Tutorial, or browse the component packages } /> } title={Build with your AI agent} href="/quickstart/connect-agents" description={ <> Reactive hooks for Vue } /> ## Next steps - Learn more about the high-level [architecture](/concepts/architecture): runtime stack and deployment modes - Compare [deployment topologies](/concepts/deployment-topologies): single process or supervised services - Run a site from the [deployment guides](/guides/deployment) - [Contribute](/support/community/contributing) # Backend stack tutorials (/tutorials/backend-stack) See the [whole stack demo at once](/tutorials/quickstart/full-stack) booting all the Worker families for a live site, mock hardware, a Gateway API, and a React dashboard in one command. ## Overview This page presents three short tutorials that build on each other. Each rung adds one layer of capability: first you watch the stack run, then you drive it, then you run a browser demo on top. | Rung | You'll | You'll end with | Mock hardware | Time | |---|---|---|---|---| | [1. Run the stack](/tutorials/backend-stack/run) | **Observe** — run one command and watch a stack come up | Device list, telemetry, and commands printed | Whatsminer M56S | `< 3 min` | | [2. Control devices from the CLI](/tutorials/backend-stack/cli) | **Interact** — drive a running stack from a REPL | Live telemetry and commands over HRPC via the kernel key file | Whatsminer M56S | `< 3 min` | | [3. Run the dashboard demo](/tutorials/full-stack/dashboard) | **Run** — launch a browser dashboard on the stack | A React dashboard with live charts at `:3030` | Whatsminer M56S | `< 15 min` after a one-time UI build | **New to MDK? Start with [1. Run the stack](/tutorials/backend-stack/run).** Each rung links to the next, so you can climb straight through. All three rungs use the same Whatsminer example to keep things simple. To see MDK working with different hardware, check out the [full site example](/tutorials/quickstart/full-stack) which runs multiple Worker families (Antminers, Whatsminers, containers, power meters) simultaneously — all using the same MDK API. Each rung is self-contained and repeats the clone-and-install step, so you can start at whichever one you need. ## Next steps - If Kernel, Worker, manager, or thing are unfamiliar, read [`terminology.md`](/reference/glossary) - Build the [smallest version of the full MDK stack: **one Worker, one Gateway route, one static HTML page](/tutorials/quickstart/build-a-dashboard) - Run the [full stack demo](/tutorials/quickstart/full-stack) # 2. Control from CLI (/tutorials/backend-stack/cli) *Get started · 2 of 3 · Control devices from the CLI* If Kernel, Worker, manager, or thing are unfamiliar, read [`terminology.md`](/reference/glossary) first. ## Overview This is rung 2 of the [Get started](/tutorials/backend-stack) ladder: **interact**. It walks the shortest path from a fresh clone to a fully wired MDK stack you can drive interactively from a CLI. Everything runs in one Node process, no real hardware required. What you'll have at the end: - A mock Whatsminer M56S serving telemetry on `127.0.0.1:14028` - An Kernel with one Worker registered and one device discovered - An interactive `client.js` REPL talking to Kernel over HRPC — pull metrics, list Workers, send commands like `reboot` and `setpower` - (Optional) A Gateway HTTP API on `:3000` so non-Node consumers (browsers, AI agents over MCP) can hit the same stack over REST Same shape as [rung 1](/tutorials/backend-stack/run): the stack still boots with `getKernel()`, `startWhatsminerWorker()`, and `kernel.registerWorker()`. What's new here is a second process — `client.js` — that connects over HRPC (using the key Kernel publishes to its key file) and drives the running stack. The example lives in [`examples/backend/mdk-e2e/`](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md#end-to-end-mdk-e2e) and contains six runnable scripts. This tutorial uses three of them: `run.js` for a smoke test, `server.js` for the long-running stack, and `client.js` for interactive control. ## Prerequisites - Node.js >=24 (LTS) - npm >=11 HRPC relies on HyperDHT for peer connectivity, including when Kernel and the Worker share a process. Review the [network requirements and checks](/guides/miners/troubleshooting#example-does-not-print-a-kernel-key) if startup stalls. ### Clone and install #### 1.1 Clone the repo ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` #### 1.2 Install dependencies The monorepo has two workspaces with their own dependency trees. Install both: ```bash backend/core/install-packages.sh ci backend/workers/install-packages.sh ci ``` Each script walks every `package.json` under its workspace and runs `npm ci`. The `examples/mdk-e2e/` package is included automatically — no extra install step needed.
(1.3 Optional) Smoke test the stack Before going interactive, prove the wiring works. `run.js` starts a mock Whatsminer + Worker + Kernel in one process, exercises a few queries, prints the results, and exits cleanly: ```bash node examples/backend/mdk-e2e/run.js ``` Expected output (the UUID and metric values vary): ```text Devices: [ '8f3e9a2b-7c1d-4e5a-9f8b-6c2d1e3f4a5b [miner-wm-m56s]' ] Telemetry: ONLINE hashrate=170000 power=3500W Commands: reboot, setPowerMode, setLED, setPowerPct, setupPools, saveComment ``` If you see those three lines, every layer is working: the mock is responding, the Worker registered the device with Kernel directly in-process (no DHT lookup), and envelope routing is delivering messages both ways. The script tears itself down and exits with code 0. If the smoke test fails with `EADDRINUSE` on port 14028, a previous run left a Node process alive. Kill stragglers with `pkill -f mdk-e2e` and retry.
### Run the interactive demo #### 3.1 Start the stack In your terminal: ```bash node examples/backend/mdk-e2e/server.js ``` `server.js` starts the same mock + Worker + Kernel as `run.js`, but stays running and prints the IDs you'll need: ```text Kernel key: 7a4c8b...e3f0 Device: 8f3e9a2b-7c1d-4e5a-9f8b-6c2d1e3f4a5b hp-rpc-cli -s 7a4c8b...e3f0 -m mdk -d '{...}' hp-rpc-cli -s 7a4c8b...e3f0 -m mdk -d '{...}' Ctrl+C to stop. ``` The `Kernel key` is a 64-char hex public key. `Device` is a UUIDv4 generated at registration time. Both vary per run — note the device UUID for the next step. The two `hp-rpc-cli` lines are paste-ready commands for inspecting Kernel over HRPC from another machine. You don't need them for this tutorial — they're there if you have [`hp-rpc-cli`](https://github.com/holepunchto/hp-rpc-cli) installed and want to go off-script. #### 3.2 Connect the interactive client Open a second terminal in the same `mdk` directory: ```bash node examples/backend/mdk-e2e/client.js ``` `client.js` reads the Kernel key from the key file Kernel published at startup (`os.tmpdir()/mdk/.kernel-key` — or pass a key as the first argument), connects over HRPC, and gives you an MDK REPL: ```text Client — connected to Kernel over HRPC: 7a4c8b… Type "help" for commands, "quit" to exit. mdk> ``` #### 3.3 Drive the stack 3.3.1 Discover the telemetry of your (mock) device by entering commands at the `mdk>` prompt, substituting `` with the UUID printed in Step 3.1: ```text mdk> metrics ``` Each command builds an MDK Protocol envelope, sends it to Kernel over HRPC, and prints the JSON response. 3.3.2 Try changing the power mode and observing the effect: ```text mdk> setpower low mdk> metrics ``` After `setpower ... low` the second `metrics` call should reflect the power mode change.
Full command reference **Reads** ```text workers — list workers list [deviceId] — list devices count [deviceId] — device count metrics — live telemetry from hardware logs — recent logs settings [deviceId] — worker settings stats [deviceId] — fleet stats config — device config (pools, etc.) capabilities — mdk-contract capabilities state [deviceId] — worker state snapshot ``` **Commands** ```text reboot — reboot miner setpower — set power mode (normal/low/high) setled [on|off] — toggle LED ``` ```text quit / exit — exit client ```
#### 3.4 Tear down When you're done, exit the client and stop the stack: ```text mdk> quit ``` Then `Ctrl+C` in Terminal 1.
### 4 (Optional) enable Gateway for HTTP access In **Terminal 1**, `Ctrl+C` the running stack, then restart with `--gateway`: ```bash node examples/backend/mdk-e2e/server.js --gateway ``` You'll see an extra line in the startup banner: ```text Gateway: http://localhost:3000 (noAuth mode) ``` Confirm it's alive — Gateway has no index page, so hit `/auth/site` directly: ```bash curl http://localhost:3000/auth/site ``` You should see something like `{"site":"Site_Name"}`. In `noAuth` mode, routes that call `services.authLib` (`/auth/miners`, `/auth/permissions`) crash with `Cannot read properties of undefined` because `authLib` is never instantiated. Routes that call `services.dataProxy` (`/auth/list-things`) fail with `UNKNOWN_METHOD` because the underlying RPC method requires the full DCS cluster registration that the production Gateway service provides. Use `/site-monitor/workers` and `/site-monitor/hashrate` for local inspection — those routes call `services.mdkClient` directly over HRPC and work without auth. `--gateway` runs in `noAuth` mode for development convenience. Do not expose port 3000 outside localhost.
## What just happened Your stack wired up, in order: 1. **Mock device**:`server.js` calls `wmMock.createServer({ port: 14028, ... })` — an HTTP server speaking the Whatsminer protocol with canned telemetry. 2. **Kernel**: `getKernel()` boots the kernel, generates a random DHT topic, and publishes its HRPC public key to the well-known key file (`os.tmpdir()/mdk/.kernel-key`). 3. **Worker**: `startWhatsminerWorker({ ..., seedDevices })` constructs a `WorkerRuntime` seeded with one device at `127.0.0.1:14028` and starts it. 4. **Registration**: `kernel.registerWorker(worker.runtime.getPublicKey())` registers the Worker with Kernel directly — no DHT round-trip because they share the process. 5. **Client**: `client.js` reads the key file from a second process, connects over HRPC, and sends MDK Protocol envelopes (`worker.list`, `telemetry.pull`, `command.request`, ...). Kernel routes them to the Worker, the Worker hits the mock, and the response flows back over HRPC. No Gateway here? Right. Gateway is the translator that lets non-Node consumers — browser UIs, AI agents over MCP — speak MDK Protocol to Kernel. `client.js` already speaks MDK Protocol over HRPC, so it talks to Kernel directly. Gateway becomes mandatory only when the consumer can't speak HRPC. See [`architecture.md#gateway`](/concepts/architecture#gateway). ## Cleanup `Ctrl+C` in Terminal 1 stops the Worker, Kernel, and mock cleanly. `run.js` deletes its own state directory on exit. `server.js` leaves data under `os.tmpdir()/mdk/` — safe to ignore, or remove with: ```bash rm -rf "$TMPDIR/mdk" /tmp/mdk ``` ## Continue Next: [3. Run the dashboard demo](/tutorials/full-stack/dashboard): put a browser dashboard on a running stack with live charts. ## Go deeper - Walk the simpler single-script path: [1. Run the stack](/tutorials/backend-stack/run) - Run a [full site (multiple Workers and devices):](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-site/site.js) - Understand the [install pattern for any Worker](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) - Read all [runnable examples in one place](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md) # 1. Run the stack (/tutorials/backend-stack/run) *Get started · 1 of 3 · Run the stack* If Kernel, Worker, manager, or thing are unfamiliar, read [`terminology.md`](/reference/glossary) first. ## Overview This is rung 1 of the [Get started](/tutorials/backend-stack) ladder: **observe**. It walks the shortest path from a fresh clone to a running Kernel with one mock Whatsminer registered, no hardware required. Everything runs in one Node process. What you'll have at the end: - A mock Whatsminer M56S serving telemetry on `127.0.0.1:14028` - An Kernel started and aware of one registered device - Device list, telemetry, and available commands printed to the terminal ## Prerequisites - Node.js >=24 (LTS) - npm >=11 HRPC relies on HyperDHT for peer connectivity, including when Kernel and the Worker share a process. Review the [network requirements and checks](/guides/miners/troubleshooting#example-does-not-print-a-kernel-key) if startup stalls. ### Clone and install #### 1.1 Clone the repo ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` #### 1.2 Install dependencies The monorepo has two workspaces with their own dependency trees. Install both: ```bash backend/core/install-packages.sh ci backend/workers/install-packages.sh ci ``` Each script walks every `package.json` under its workspace and runs `npm ci`. ### Run the example ```bash node examples/backend/mdk-e2e/run.js ``` Expected output: ```text Devices: [ 'WM-001 [miner-wm-m56s]' ] Telemetry: mining hashrate=295764693.45 power=4000W Commands: reboot, setPowerMode, setLED, setupPools, setPowerPct, setUpfreqSpeed, registerThing, updateThing, forgetThings, saveSettings, saveComment, editComment, deleteComment ``` If you see those three lines, the whole stack is up: mock device responding, Worker registered, Kernel started and aware of the device. The script queries the device and then exits cleanly. If the example fails with `EADDRINUSE`, a previous run left port 14028 bound. Kill stale Node processes with `pkill -f mdk-e2e` and retry. ## What just happened Here is what `run.js` does — the minimum runnable shape for an MDK stack: ```js const { getKernel, waitForDiscovery } = require('../../../backend/core/mdk') const { createMdkClient } = require('../../../backend/core/client') const { startWhatsminerWorker } = require('../../../backend/workers/miners/whatsminer') const wmMock = require('../../../backend/workers/miners/whatsminer/mock/server') // Start a mock Whatsminer M56S on port 14028 wmMock.createServer({ port: 14028, host: '127.0.0.1', type: 'm56s', serial: 'WM-001', password: 'admin' }) // Start the Whatsminer Worker with one seeded device const worker = await startWhatsminerWorker({ workerId: 'whatsminer-m56s-e2e', model: 'm56s', storeDir: path.join(ROOT, 'worker-store'), kernelTopic: TOPIC, seedDevices: [{ id: 'WM-001', info: { serialNum: 'WM-001' }, opts: { address: '127.0.0.1', port: 14028, password: 'admin' } }] }) // Start Kernel — discovers the Worker via DHT const kernel = await getKernel({ root: ROOT, topic: TOPIC }) await waitForDiscovery(kernel) // Connect an MDK client over HRPC const client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }) await client.connect() // Query the device and print results const list = await client.pullTelemetry(deviceId, 'list') const tel = await client.pullTelemetry(deviceId, 'metrics') const caps = await client.getCapabilities(deviceId) ``` Six steps, in order: 1. **Starts a mock miner.** `wmMock.createServer({ port: 14028, type: 'm56s', serial: 'WM-001', password: 'admin' })` binds port 14028 with a Whatsminer-compatible API serving canned telemetry. 2. **Starts a Worker.** `startWhatsminerWorker({ ...seedDevices })` instantiates the Whatsminer manager and seeds it with one device registration at `127.0.0.1:14028`. The Worker stores the registration and begins polling the mock. 3. **Starts Kernel.** `getKernel({ topic })` brings up the kernel and joins the DHT topic. It discovers the Worker and pulls its identity and capabilities. 4. **Waits for discovery.** `waitForDiscovery(kernel)` blocks until the Worker appears in Kernel's registry. 5. **Connects a client.** `createMdkClient({ hrpc: { key: kernel.getPublicKey() } })` creates an MDK Protocol client that speaks to Kernel over HRPC. 6. **Queries and prints.** The script pulls device list, telemetry, and capabilities, prints them, cleans up, and exits. No Gateway here? Right. Gateway is the translator that lets non-Node consumers — browser UIs, AI agents over MCP — speak MDK Protocol to Kernel. This script holds the Kernel handle in-process and speaks MDK Protocol to it directly. See [`architecture.md#gateway`](/concepts/architecture#gateway) for when Gateway is mandatory. ## Cleanup The script cleans up automatically on exit. Temporary data lives at `os.tmpdir()/e2e-run/` and is removed by the script. If needed, you can manually clean with: ```bash rm -rf "$TMPDIR/e2e-run" /tmp/e2e-run ``` ## Continue Next: [2. Control devices from the CLI](/tutorials/backend-stack/cli) — keep a stack running and drive it interactively (Whatsminer plus an MDK REPL). ## Next steps - Try different miner hardware — the same MDK API works with [Antminers](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md), [Avalonminers](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md), and more - Run a full site (5 Workers, 26 devices) — [`examples/backend/mdk-site/site.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-site/site.js) - See Kernel and a Worker as separate OS processes — [`dht-worker.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-worker.js) + [`dht-kernel.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-kernel.js) + [`client.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/client.js) - Understand the install pattern for any Worker — [`backend/workers/docs/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) - Read all runnable examples in one place — [`examples/backend/README.md`](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md) # Run a demo from stack to dashboard (/tutorials/full-stack/dashboard) *Get started · 3 of 3 · Run the dashboard demo* If Kernel, Worker, manager, or thing are unfamiliar, read [`terminology.md`](/reference/glossary) first. ## Overview This is rung 3 of the [Get started](/tutorials/backend-stack) ladder: **run**. It puts a real browser dashboard on top of the stack you already know, using the [`mdk-site-monitor`](https://github.com/tetherto/mdk/tree/main/examples/e2e) example. What you'll have at the end: - The same mock Whatsminer M56S stack from [rung 2](/tutorials/backend-stack/cli), fronted by a Gateway REST API on `:3000` - A React dashboard on `:3030` — total hashrate, total power, and active device count as metric cards, a live hashrate chart, and a per-device breakdown - The dashboard polling Gateway every 5 seconds, built entirely from MDK UI components (`MetricCard`, `HashRateLineChart`, `Typography`, ...) Same shape as rungs 1 and 2: underneath, the stack still boots with `getKernel()`, `startWhatsminerWorker()`, and `kernel.registerWorker()`, and the device is the same Whatsminer M56S mock as [rung 2](/tutorials/backend-stack/cli). What's new is two layers on top — Gateway translating MDK Protocol to REST, and a Vite React UI consuming that REST. ## Prerequisites - Node.js >=24 (LTS) - npm >=11 HRPC relies on HyperDHT for peer connectivity, including when Kernel and the Worker share a process. Review the [network requirements and checks](/guides/miners/troubleshooting#example-does-not-print-a-kernel-key) if startup stalls. ### Clone and install the stack #### 1.1 Clone the repo ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` #### 1.2 Install dependencies ```bash backend/core/install-packages.sh ci backend/workers/install-packages.sh ci ``` ### Build the UI packages (one time) The dashboard imports pre-built MDK UI packages from `ui`. Build them once: ```bash cd ui npm install npm run build cd - ``` This is the slow step — it compiles the UI workspace. You only do it once; re-running the dashboard later skips straight to the next step. ### Install the dashboard's dependencies ```bash cd examples/e2e/ui npm install cd - ``` ### Start everything `start.js` is an interactive launcher. From the repo root: ```bash node examples/e2e/start.js ``` It prints the available services and a prompt. Type `start all` to launch the backend and UI together: ```text mdk> start all ``` This starts: 1. **Gateway** (`server.js --gateway`) — Kernel + mock Whatsminer + a REST API on `http://localhost:3000`, in `noAuth` mode 2. **UI** — a Vite dev server on `http://localhost:3030`, launched with `VITE_NO_AUTH=true` so it skips the login screen Wait a few seconds for both to come up, then open: ```text http://localhost:3030 ``` You'll see the dashboard populate within ~5 seconds (its first poll), then update live. Other launcher commands: ```text status — show running services and their URLs stop [kernel|gateway|ui|all] — stop a service (default: all) start [kernel|gateway|ui|all] — start a service (default: all) help — show usage exit — stop everything and quit ``` `start all` runs Gateway in `noAuth` mode for development convenience. Do not expose port 3000 outside localhost. ## What you'll see ```mermaid flowchart TD mock["Mock Whatsminer M56S (TCP :14028)"] --> worker["MDK Worker"] worker --> kernel["Kernel (HRPC)"] kernel --> gateway["Gateway (HTTP :3000)"] gateway -->|"GET /site-monitor/hashrate every 5s"| ui["Dashboard (Vite :3030)"] ``` The page renders three metric cards (Total Hashrate in TH/s, Total Power in W, Active Devices), a live hashrate line chart that grows as new data points arrive, and a per-device table. All of it comes from `@tetherto/mdk-react-devkit` components driven by the `@tetherto/mdk-react-adapter` data hooks — no hand-rolled UI. See [`SiteHashratePage.tsx`](https://github.com/tetherto/mdk/blob/main/examples/e2e/ui/src/SiteHashratePage.tsx) for the page source. ## Cleanup At the launcher prompt: ```text mdk> exit ``` `exit` stops Gateway, the mock, Kernel, and the UI dev server. `server.js` leaves data under `os.tmpdir()/mdk-e2e/` — safe to ignore, or remove with: ```bash rm -rf "$TMPDIR/mdk-e2e" /tmp/mdk-e2e ```
Add real auth (Google OAuth) The `start all` shortcut above runs in `noAuth` mode so you can see the dashboard immediately. To exercise the real authentication flow — a Google sign-in that issues a bearer token the dashboard sends on every request — configure OAuth and start the services without the `noAuth` shortcut. **1. Create a Google OAuth 2.0 client** 1. Go to [Google Cloud Console API credentials](https://console.cloud.google.com/apis/credentials) 2. Create an **OAuth 2.0 Client ID** (type: *Web application*) 3. Add to **Authorized redirect URIs**: `http://localhost:3000/oauth/google/callback` 4. Add to **Authorized JavaScript origins**: `http://localhost:3030` 5. Copy the **Client ID** and **Client Secret** **2. Configure Gateway** Edit `backend/core/gateway/config/facs/httpd-oauth2.config.json` (copy from `.example` if it doesn't exist) and fill in your client ID and secret: ```json { "h0": { "method": "google", "credentials": { "client": { "id": "", "secret": "" } }, "startRedirectPath": "/oauth/google", "callbackUri": "http://localhost:3000/oauth/google/callback", "callbackUriUI": "http://localhost:3000", "users": [] } } ``` **3. Set yourself as super-admin** In `backend/core/gateway/config/facs/auth.config.json`, set `superAdmin` to your Google account email: ```json { "a0": { "superAdmin": "you@example.com" } } ``` **4. Run with auth** Start Gateway and the UI without the `VITE_NO_AUTH` shortcut (run them directly rather than via `start all`), open `http://localhost:3030`, click **Sign in with Google**, and authorise with the super-admin email. The dashboard then shows live data behind the token. For the full setup, see the example's [`README.md`](https://github.com/tetherto/mdk/tree/main/examples/e2e).
## Continue Previous: [2. Control devices from the CLI](/tutorials/backend-stack/cli) You've climbed the whole ladder: observed a stack, driven it from a CLI, and built a live UI on it. ## Go deeper - The [full example, including the OAuth setup and how to add a new data panel](https://github.com/tetherto/mdk/tree/main/examples/e2e) - Learn more about [MDK UI toolkit (components, hooks, theming)](https://github.com/tetherto/mdk/blob/main/ui/README.md) - [Run a full site (multiple Workers and devices)](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-site/site.js) - Read [all runnable examples in one place](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md) # Build a minimal single-page dashboard (/tutorials/quickstart/build-a-dashboard) # Build a minimal single-page dashboard [`examples/full-site`](https://github.com/tetherto/mdk/tree/main/examples/full-site) is the fullest demonstration of the MDK stack: multiple Worker processes spanning miners, containers, power meters, sensors, and miner pools, plus a multi-page React + Vite dashboard built on `@tetherto/mdk-react-devkit`. That breadth is the point of that example — but it also means it's a lot to read through if all you want is to understand (or prototype against) the actual wiring: Kernel, one Worker, a Gateway route, a page that polls it. This guide builds the smallest version of that same shape: **one Worker, one Gateway route, one React page**. It teaches the same end-to-end shape as `examples/full-site` — including its UI layer, `@tetherto/mdk-react-devkit` components driven by `@tetherto/mdk-react-adapter` hooks — without that example's family-specific adapters, persistence, history, commands, multi-page router, or chart aggregation. If **Kernel**, **Worker**, or **Gateway** are unfamiliar terms, read [the architecture overview](/concepts/architecture) first. This tutorial also assumes you've skimmed [Build a third-party Worker](/guides/workers/build-a-worker) — the one Worker used here is [`backend/workers/samples/demo-worker`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/index.js), the same zero-dependency reference Worker that tutorial is built around. ## Overview This tutorial builds the smallest version of the full MDK stack: **one Worker, one Gateway route, one React page**. What you'll have at the end: ```text examples/minimal-dashboard/ package.json start.js # boots Kernel + the one Worker + Gateway, one process plugins/ dashboard/ mdk-plugin.json # one route: GET /overview controllers/ overview.js # lists every Worker's devices + live telemetry ui/ package.json tsconfig.json vite.config.ts index.html src/ main.tsx # + render OverviewPage OverviewPage.tsx # polls /overview, renders devkit components ``` No router, no charts, no hand-rolled CSS — one page built from three `@tetherto/mdk-react-devkit` primitives (`LabeledCard`, `DataTable`, `Badge`) and one `@tetherto/mdk-react-adapter` hook (`useQuery`), the same building blocks `examples/full-site/ui` uses, minus the parts (router, sidebar, line charts, domain panels) this single-page, single-Worker dashboard doesn't need. ## Prerequisites - Node.js `>=24` - This repo checked out, with the backend and UI dependencies installed once from the repository root: ```bash npm run setup:core npm run setup:workers npm run setup:ui npm run build:ui ``` Running `npm run setup` from `examples/full-site` is also supported, but it is broader: it installs these backend and UI dependencies plus the full-site example and UI dependencies and builds the UI packages. - Commands below assume a new `examples/minimal-dashboard/` directory alongside `examples/full-site/` ### Pick the Worker Use [`backend/workers/samples/demo-worker`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/index.js) as-is — it needs no worker-infra services (provisioning stores, alert templates), just `WorkerRuntime` and its own bundled mock device. Nothing in the steps below is specific to it, though: swap in `startWhatsminerWorker`, `startAntminerWorker`, or your own Worker from [Build a third-party Worker](/guides/workers/build-a-worker) and everything past the next step is unchanged. ### Boot Kernel and Worker in the same process The simplest of the three discovery modes ([full trade-offs here](/concepts/deployment-topologies#the-trade-off)): one Node process owns both the Kernel and the Worker, so there's no key file or DHT topic to manage. `examples/minimal-dashboard/start.js`: ```js 'use strict' const path = require('path') const { getKernel, waitForDiscovery } = require('../../backend/core/mdk') const { startDemoWorker } = require('../backend/demo-worker-caller') const demoMock = require('../../backend/workers/samples/demo-worker/mock/server') const ROOT = path.join(__dirname, '.mdk-data') const MOCK_PORT = 9101 const HTTP_PORT = Number(process.env.MDK_HTTP_PORT) || 3000 function onceListening (mock) { if (mock.server.listening) return Promise.resolve() return new Promise((resolve) => mock.server.once('listening', resolve)) } async function main () { // the one fake device this dashboard will show — swap for real hardware later const mock = demoMock.createServer({ host: '127.0.0.1', port: MOCK_PORT, serial: 'WM3-0001' }) await onceListening(mock) // Kernel + the one Worker, same process const kernel = await getKernel({ root: ROOT }) const worker = await startDemoWorker({ workerId: 'demo-worker-1', storeDir: path.join(ROOT, 'demo-worker-store'), seedDevices: [{ id: 'demo-0', opts: { host: '127.0.0.1', port: MOCK_PORT } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) await waitForDiscovery(kernel, { minWorkers: 1 }) console.log('worker registered: %s', worker.deviceIds.join(', ')) } module.exports = { main, ROOT, HTTP_PORT } if (require.main === module) main().catch((err) => { console.error(err); process.exit(1) }) ``` The demo Worker package exports a plugin and SQLite helper, not a boot function. The separate [`demo-worker-caller`](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js) used here owns `WorkerRuntime`, device configuration, persistence, sampling, and shutdown. `getKernel({ root: ROOT })` with no `topic`/`discovery` option defaults to DHT discovery with a fresh random topic. This example then registers the Worker's public key directly because both objects are in the same process. See the [discovery model](/concepts/stack/workers#discovery-model) for the DHT, Local, and Same-process options to use in other deployments. ### Write the one Gateway plugin route A [Gateway plugin](/guides/gateway/plugins) is a directory with a manifest and a controller. This one has a single read-only route that lists every registered Worker's devices and pulls each one's default `metrics` telemetry bundle — no per-device-family branching, because there's only one family here. #### 3.1 Write the plugin manifest `examples/minimal-dashboard/plugins/dashboard/mdk-plugin.json`: ```json { "name": "@your-org/mdk-plugin-dashboard", "version": "0.1.0", "description": "Minimal dashboard plugin: one route that lists every registered device and its live telemetry.", "routes": [ { "id": "dashboard.overview", "handler": "./controllers/overview.js", "auth": false, "http": { "method": "GET", "path": "/overview" }, "description": "Live snapshot of every device across every registered Worker.", "safety": "read-only" } ] } ``` #### 3.2 Write the controller `examples/minimal-dashboard/plugins/dashboard/controllers/overview.js`: ```js 'use strict' module.exports = async function overview (req, services) { const { workers } = await services.mdkClient.listWorkers() const devices = await Promise.all( workers.flatMap((w) => (w.deviceIds || []).map(async (deviceId) => { const tel = await services.mdkClient.pullTelemetry(deviceId, 'metrics') return { deviceId, workerId: w.workerId, workerState: w.state, ...tel.metrics } })) ) return { ts: Date.now(), devices } } ``` A controller is `async (req, services) => value` — return a plain object, the Gateway serializes it to JSON itself; you never touch `res`. `services.mdkClient` is the same client used everywhere else in MDK — no knowledge of the underlying MDK Protocol envelope required. With more than one Worker family mixed in (miners, powermeters, sensors, ...) you'd branch by `deviceFamily` instead of spreading `tel.metrics` blindly — see [`examples/full-site/plugins/site/lib/site.js`](https://github.com/tetherto/mdk/blob/main/examples/full-site/plugins/site/lib/site.js) for that pattern once you outgrow this one. ### Serve the plugin and static page from the same Gateway Add `startGateway` to `start.js`, mounting the plugin via `extraPluginDirs` and the built UI (Step 5's `ui/dist`) via `common.staticRootPath`. This is the complete canonical file; its boot order is mock, Kernel, Worker, registration, readiness, then Gateway: ```js 'use strict' const path = require('path') const { getKernel, startGateway, waitForDiscovery } = require('../../backend/core/mdk') const { startDemoWorker } = require('../backend/demo-worker-caller') const demoMock = require('../../backend/workers/samples/demo-worker/mock/server') const ROOT = path.join(__dirname, '.mdk-data') const MOCK_PORT = 9101 const HTTP_PORT = Number(process.env.MDK_HTTP_PORT) || 3000 function onceListening (mock) { if (mock.server.listening) return Promise.resolve() return new Promise((resolve) => mock.server.once('listening', resolve)) } async function main () { // Start the mock device before its Worker tries to connect. const mock = demoMock.createServer({ host: '127.0.0.1', port: MOCK_PORT, serial: 'WM3-0001' }) await onceListening(mock) // Start Kernel, then the Worker runtime that hosts the demo Worker plugin. const kernel = await getKernel({ root: ROOT }) const worker = await startDemoWorker({ workerId: 'demo-worker-1', storeDir: path.join(ROOT, 'demo-worker-store'), seedDevices: [{ id: 'demo-0', opts: { host: '127.0.0.1', port: MOCK_PORT } }] }) // Register the Worker and wait until it is ready before accepting HTTP traffic. await kernel.registerWorker(worker.runtime.getPublicKey()) await waitForDiscovery(kernel, { minWorkers: 1 }) await startGateway({ kernel, noAuth: true, port: HTTP_PORT, root: path.join(ROOT, 'gateway'), tmpdir: path.join(ROOT, 'gateway'), extraPluginDirs: [path.join(__dirname, 'plugins', 'dashboard')], common: { staticRootPath: path.join(__dirname, 'ui', 'dist') } }) console.log('worker registered: %s', worker.deviceIds.join(', ')) console.log(`dashboard up: http://localhost:${HTTP_PORT}/`) } module.exports = { main, ROOT, HTTP_PORT } if (require.main === module) main().catch((err) => { console.error(err); process.exit(1) }) ``` The route's `"auth": false` and Gateway's `noAuth: true` are **local-development settings**. Do not expose this Gateway directly to an untrusted network. Production deployments require TLS termination, authentication, authorization, and network policy. Before production, remove `noAuth: true`, set the route's `"auth"` to `true`, and declare its required `"permissions"`. Write routes also need input validation, rate limits, and auditing. Serve the built page from `common.staticRootPath`, not a separate server on its own port. Gateway plugin controllers only receive `(req, services)` — never the underlying reply object — so a controller has no way to set `Access-Control-Allow-Origin`, and Gateway has no built-in CORS support. Building the UI (Step 5) and serving `ui/dist` from `staticRootPath` keeps `fetch('/overview')` same-origin with zero CORS configuration. This is also why `examples/full-site`'s Vite *dev* server proxies `/site/*` to the Gateway port instead of calling it cross-origin — Step 5's `vite.config.ts` proxies `/overview` the same way for hot-reload development. ### Write the single-page UI with MDK devkit components Instead of hand-rolled HTML, the page is a small React + Vite app built from the same packages `examples/full-site/ui` uses — `@tetherto/mdk-react-adapter` for the provider and data hook, `@tetherto/mdk-react-devkit` for the components — scaled down to what one route needs: no router (one page), no charts (no history endpoint here), no sidebar. Three primitives do the job: `LabeledCard` for the section container, `DataTable` for the sortable device grid, and `Badge` to color-code `workerState`. `examples/minimal-dashboard/ui/package.json`: ```json { "name": "@your-org/mdk-minimal-dashboard-ui", "type": "module", "version": "0.1.0", "private": true, "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build" }, "dependencies": { "@tetherto/mdk-react-adapter": "file:../../../ui/packages/react-adapter", "@tetherto/mdk-react-devkit": "file:../../../ui/packages/react-devkit", "react": "^19.2.0", "react-dom": "^19.2.0" }, "devDependencies": { "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.3", "vite": "^6.3.5" } } ``` `examples/minimal-dashboard/ui/tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "jsx": "react-jsx", "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["vite/client"], "module": "ESNext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*", "vite.config.ts"], "exclude": ["node_modules", "dist"] } ``` `examples/minimal-dashboard/ui/vite.config.ts` — the dev-only proxy so `fetch('/overview')` stays same-origin against the Gateway port, the same pattern [`examples/full-site/ui/vite.config.ts`](https://github.com/tetherto/mdk/blob/main/examples/full-site/ui/vite.config.ts) uses for `/site/*`: ```ts declare const process: { env: Record } const apiPort = process.env.VITE_API_PORT || '3000' plugins: [react()], server: { port: Number(process.env.MDK_UI_PORT) || 3041, proxy: { '/overview': `http://localhost:${apiPort}` } } }) ``` `examples/minimal-dashboard/ui/index.html`: ```html MDK dashboard
``` `examples/minimal-dashboard/ui/src/main.tsx` — `` wires the TanStack Query client `useQuery` needs and resolves the API base URL, exactly as it does in [`examples/full-site/ui/src/main.tsx`](https://github.com/tetherto/mdk/blob/main/examples/full-site/ui/src/main.tsx): ```tsx const rootElement = document.getElementById('root') if (!rootElement) throw new Error('ERR_ROOT_ELEMENT_MISSING') // Same-origin: the Vite dev proxy forwards /overview to the gateway; // in production the Gateway serves this build from staticRootPath. ReactDOM.createRoot(rootElement).render( ) ``` `examples/minimal-dashboard/ui/src/OverviewPage.tsx` — `useQuery` polls the route from Step 3 the same way [`examples/full-site/ui/src/SitePage.tsx`](https://github.com/tetherto/mdk/blob/main/examples/full-site/ui/src/SitePage.tsx) polls `/site/overview`; `DataTable` and `Badge` replace that page's hand-rolled markup: ```tsx type Device = { deviceId: string workerId: string workerState: string hashrate_rt?: number power?: number temperature?: number } type Overview = { ts: number; devices: Device[] } function get(base: string, path: string): Promise { return fetch(`${base}${path}`).then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() as Promise }) } function stateBadgeStatus(state: string): 'success' | 'error' | 'default' { if (state === 'ready') return 'success' if (state === 'offline') return 'error' return 'default' } const columns: DataTableColumnDef[] = [ { accessorKey: 'deviceId', header: 'Device' }, { accessorKey: 'workerId', header: 'Worker' }, { id: 'workerState', header: 'Worker state', cell: ({ row }) => { const state = (row.original.workerState || 'unknown').toLowerCase() return } }, { accessorKey: 'hashrate_rt', header: 'Hashrate (TH/s)', cell: ({ getValue }) => (Number(getValue()) || 0).toFixed(2) }, { accessorKey: 'power', header: 'Power (W)', cell: ({ getValue }) => (Number(getValue()) || 0).toFixed(0) }, { accessorKey: 'temperature', header: 'Temp (°C)', cell: ({ getValue }) => (Number(getValue()) || 0).toFixed(1) } ] const { apiBaseUrl } = useMdkContext() const overview = useQuery({ queryKey: ['dashboard-overview'], queryFn: () => get(apiBaseUrl, '/overview'), refetchInterval: 3000 }) return (
data={overview.data?.devices ?? []} columns={columns} getRowId={(row) => row.deviceId} loading={overview.isLoading} enablePagination={false} />
) } ``` A controller is `async (req, services) => value`; a page is `useQuery` + devkit components — neither one touches the other's plumbing. `OverviewPage` never imports `mdkClient`, and `overview.js` never imports React.
### Run it #### 6.1 Add package.json `examples/minimal-dashboard/package.json`: ```json { "name": "@your-org/mdk-minimal-dashboard", "version": "0.1.0", "private": true, "scripts": { "build": "npm --prefix ui run build", "start": "node start.js" } } ``` #### 6.2 Start the dashboard Install each package's own dependencies, build the UI once, then boot the backend: ```bash cd examples/minimal-dashboard npm install npm --prefix ui install npm run build npm run start ``` Open `http://localhost:3000/` — the page polls `/overview` every 3 seconds and shows the one `demo-0` device reporting live telemetry from its mock. Confirm the API directly with: ```bash curl -s http://localhost:3000/overview ``` You should see JSON shaped like `{ "ts": ..., "devices": [{ "deviceId": "demo-0", "workerId": "demo-worker-1", "workerState": "READY", "hashrate_rt": ..., "power": ..., "temperature": ... }] }`. ### Hot-reload UI development Vite does **not** replace the Gateway. Without hot reload you open `:3000` (Gateway serves the built `ui/dist`). With hot reload you still need the Gateway for `/overview`, and Vite only serves the React app and proxies that path. Keep **both** terminals running: **Terminal 1 — Gateway** (do not stop this): ```bash cd examples/minimal-dashboard npm run start ``` Wait for `dashboard up: http://localhost:3000/`. **Terminal 2 — Vite**: ```bash cd examples/minimal-dashboard VITE_API_PORT=3000 npm --prefix ui run dev ``` Open **`http://localhost:3041/`** (the Vite port), not `:3000`. Step 5's proxy forwards `/overview` to the Gateway on `VITE_API_PORT`. If Terminal 1 is down, Vite logs `http proxy error: /overview` / `ECONNREFUSED` and the table stays empty.
## Next steps - **More Workers, zero controller changes**: `overview.js` already loops over every registered Worker generically. Register a second Worker the same way (Step 2's `startDemoWorker` + `startWhatsminerWorker`, etc., both followed by `kernel.registerWorker(...)`) and it appears in `/overview` for free. - **A write route**: Add a second manifest entry (`POST /devices/{deviceId}/command`) calling `services.mdkClient.sendCommand(deviceId, 'setPowerMode', { mode })`, following the `command.js` example in [Gateway plugins](/guides/gateway/plugins), and a `Button` in `OverviewPage.tsx` that `POST`s to it. Before deploying any physical write command, require narrowly scoped authorization, validate its payload and target state, apply rate limits, and record an audit trail. - **More pages, charts, history**: Add `react-router-dom` and a second route/page, or graduate to the domain layer (`@tetherto/mdk-react-devkit/domain`'s `LineChartCard`, `MetricCard`, header stats bar) once the Gateway plugin grows a history endpoint to feed them — see [`examples/full-site/ui/src/DashboardPage.tsx`](https://github.com/tetherto/mdk/blob/main/examples/full-site/ui/src/DashboardPage.tsx) and [`examples/full-site/plugins/site/controllers/history.js`](https://github.com/tetherto/mdk/blob/main/examples/full-site/plugins/site/controllers/history.js) for that shape. - **Separate processes or hosts**: Replace the direct registration in Step 2 with Local discovery for processes on one machine or DHT discovery for separate hosts, as described in the [discovery model](/concepts/stack/workers#discovery-model). ## Troubleshooting | Symptom | Cause | | --- | --- | | `/overview` returns `{ devices: [] }` | `kernel.registerWorker(...)` wasn't awaited, or `startGateway` was called before `waitForDiscovery` resolved | | `/overview` returns `500` / `Cannot read properties of null` | Controller spread `tel.metrics` when `pullTelemetry` returned `null` — use `(tel && tel.metrics) \|\| {}` as in Step 3 | | `pullTelemetry` throws / device shows zeros | The Worker's `connect()` couldn't reach the mock at boot — confirm the mock's `listening` event fired before `startDemoWorker` seeded it (Step 2's `onceListening`) | | Vite shows `http proxy error: /overview` / `ECONNREFUSED`, page at `:3041` has no data | Gateway is not listening on the proxy target — keep `npm run start` running in another terminal, and set `VITE_API_PORT` to that Gateway port (default `3000`). Open `:3041`, not `:3000` | | Browser `fetch('/overview')` fails from Vite with CORS / wrong host when Gateway is up | See the CORS note in Step 4 — the Vite proxy must target the Gateway (`VITE_API_PORT`); do not call a different origin from the page | | `ERR_PLUGIN_HANDLER_NOT_FOUND: routes.dashboard.overview: ./controllers/overview.js` on Gateway boot | `extraPluginDirs` must point at the directory *containing* `mdk-plugin.json`, not the controller file itself | | Gateway boots but the page 404s | `common.staticRootPath` must be an absolute path (`path.join(__dirname, 'ui', 'dist')`) pointing at a *built* UI (`npm run build` in Step 6), not a relative string or the unbuilt `ui/src` | | `Cannot find module '@tetherto/mdk-react-devkit'` when building `ui/` | Run the Prerequisites' `npm run setup:ui && npm run build:ui` from the repo root first — the UI packages ship pre-built `dist/` output that `ui/`'s `package.json` depends on via `file:` links | # Run the full-site example (/tutorials/quickstart/full-stack-demo) ## Overview This tutorial presents the [full-site example](https://github.com/tetherto/mdk/tree/main/examples/full-site) end to end running the full MDK stack: multiple configured Workers across a range of device families, their mock device servers, a Gateway HTTP API, and a React dashboard, all in one command. What you'll have at the end: - The supported fleet: multiple miner types, container types, and power meters; one sensor family with two inlet sensors; and two mining pools, each backed by mock hardware that speaks the real wire protocol - A Gateway API at `:3007` serving `/site/overview`, `/site/history`, and `/site/miners/:id/command` - A React dashboard at `:3040` with live hashrate, power, and device status - An MCP server exposing the site's device registry, telemetry, and commands as tools for AI agents The example can boot in one Node.js process (`node start.js`) or as separate processes through an interactive REPL (`node cli.js`). This tutorial uses `start.js` — the simplest path. ## Prerequisites - Node.js >=24 (LTS) - npm >=11 ### Install the example #### 1.1 Clone the repo ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` #### 1.2 Run setup ```bash cd examples/full-site npm run setup ``` `setup` installs `backend/core`, `backend/workers`, the UI workspace devkit packages, and this example's own dependencies. It runs once; subsequent starts skip it. The script walks several workspaces; first run takes 1-2 minutes. ### Start the site Start with a small fleet (3 miners per family, 9 total) for a fast first boot: ```bash node start.js --miners 3 ``` Wait for the terminal to print the Gateway and UI URLs: ```text Gateway http://localhost:3007 UI http://localhost:3040 ``` Open `http://localhost:3040` in a browser. The dashboard shows live hashrate, total power, and a per-device breakdown. Verify via the API: ```bash curl -s http://localhost:3007/site/overview | jq '{miners: (.miners|length), containers: (.containers|length), pools: (.pools|length), sensors: (.sensors|length)}' ``` Expected output: ```text { "miners": 9, "containers": 2, "pools": 2, "sensors": 2 } ``` The default fleet (100 miners per family, 300 total) opens 900+ file descriptors simultaneously. Before switching from `--miners 3` to the full fleet, run `ulimit -n 4096` in the same terminal session. ## What just happened 1. **Setup** installed `backend/core`, `backend/workers`, the MDK UI devkit, and this example — all the packages `start.js` imports at boot. 2. **Mock hardware** — `mocks.js` started one server per device family (miners on TCP, containers on HTTP and MQTT, power meters and sensors on Modbus, pools on REST). They speak the real wire protocols; every Worker driver runs its true connect, collect, and command paths against them. 3. **Kernel** — `getKernel()` started the orchestration layer that discovers Workers as they register and routes telemetry pulls and commands to them. 4. **Workers** — eleven `bootWorker()` calls, one per configured Worker spec, each dispatching to that family's `start{X}Worker()` boot function (`startWhatsminerWorker`, `startAvalonWorker`, ...) to construct a `WorkerRuntime` and connect it to the Kernel. 5. **Gateway** — `startGateway()` mounted the site plugin from `plugins/site/` and opened the HTTP server on `:3007`. The plugin aggregates data across the configured Workers through `mdkClient`. 6. **UI** — a Vite React dev server started on `:3040`, serving a dashboard built from MDK devkit components. 7. **MCP server** — an MCP server started over HRPC, exposing the site's device registry, telemetry, and command dispatch as tools for AI agents. ## Cleanup `Ctrl+C` stops mocks, Workers, Kernel, Gateway, and the UI dev server cleanly. State (Kernel key, Worker seeds, device registry, and tail-log history) persists in `.mdk-data/`. To wipe it: ```bash rm -rf examples/full-site/.mdk-data ``` ## Next steps - If Kernel, Gateway, Worker, manager, or thing are unfamiliar, read [terminology](/reference/glossary) - Run the same supported Worker fleet under PM2 or Docker — [Multi-process deployment guide](/guides/deployment/run-all-workers-site) - Add [custom plugins to the Gateway HTTP API](/guides/gateway/plugins) - Connect an [AI agent to the MCP server](https://github.com/tetherto/mdk/blob/main/examples/full-site/docs/mcp-server.md) to query and command the site - Explore [the interactive CLI process manager](https://github.com/tetherto/mdk/tree/main/examples/full-site) for per-process logs, runtime seeding, and live status # Explore the demo (/tutorials/ui/explore-the-demo) This page presents a copy/paste method to clone the MDK UI monorepo, run the demo, and browse the full component library live. ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ ## Run the demo One copy/paste: four steps — clone, install, build, run. ```bash # Requires an SSH key configured for your GitHub account git clone git@github.com:tetherto/mdk.git cd mdk/ui npm install && npm run build npm run dev:catalog ``` ```bash git clone https://github.com/tetherto/mdk.git cd mdk/ui npm install && npm run build npm run dev:catalog ``` Open [http://localhost:5173/mdk](http://localhost:5173/mdk). You'll land in the demo browser — a sidebar of the full MDK component library, covering core primitives (buttons, inputs, tables, charts, dialogs) and mining-domain components (operations centre, vendor containers, alerts, pool manager, settings). ### What the demo wires The demo app uses [`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation), `@tetherto/mdk-react-adapter`, and `@tetherto/mdk-react-devkit` with ``. ## Next steps - [Wire React](/quickstart/wire-react): install all three packages and wrap your app in `MdkProvider` - [Wire a React app](/tutorials/ui/react/tutorial): scaffold an app with `MdkProvider` and adapter hooks - [Learn more about the React MDK UI Devkit](/tutorials/ui/react) # Get started with React (/tutorials/ui/react) ## TL;DR - **One provider, zero Redux**: install and wrap your app in `` so connected foundation components and adapter hooks share the same stores and API client - Presentational [`/core` and `/foundation`](/reference/ui/components) imports can work without the provider - Anything that reads app state needs the provider ## Choose your path } title={Quickstart} href="/quickstart/wire-react" description={ <> Install all three packages, wrap in MdkProvider, wire stores and theming ⏱️ <3 min } /> } title={Explore the demo} href="/tutorials/ui/explore-the-demo" description={ <> Single copy/paste to clone the monorepo and browse the full demo app in your browser ⏱️ <1 min } /> } title={Tutorial} href="/tutorials/ui/react/tutorial" description={ <> Step-by-step app scaffold with MdkProvider, adapter hooks, and foundation components ⏱️ <5 min } /> } title={Build dashboards with your AI agent} href="/quickstart/connect-agents" description={ Wire Cursor or Claude to MDK with the UI CLI, then build from plain-language prompts } /> ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ ## About the React stack The React UI Devkit is published as **three workspace packages** in the [MDK monorepo](https://github.com/tetherto/mdk): - **[`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation)** — headless state (Zustand vanilla stores), a TanStack `QueryClient` factory, telemetry primitives, and the command state machine. No React. - **`@tetherto/mdk-react-adapter`** — React bindings: ``, store hooks (`useAuth`, `useDevices`, `useNotifications`, `useTimezone`, `useActions`) and re-exports of `useQuery` / `useMutation`. - **`@tetherto/mdk-react-devkit`** — the React UI library: `./core` primitives and `./foundation` mining-domain components, hooks, and styles. `@tetherto/mdk-ui-foundation` is framework-agnostic; the adapter and devkit are React-specific. Add all three to your app’s `package.json`, then wrap the tree once in **``** from `@tetherto/mdk-react-adapter`. **`@tetherto/mdk-ui-foundation`** is the brain in the back room. It remembers who is logged in, which miners are selected, and what timezone the operator uses — but it has no buttons and no React. Other code (including non-React utilities) can read and update it with `getState()` / `setState()`. **`@tetherto/mdk-react-adapter`** plugs that brain into React. `` turns it on for your whole app. Hooks like `useAuth` and `useDevices` let components listen to one slice of state and re-render when that slice changes. **[`/core`](/reference/ui/components)** (`import … from '@tetherto/mdk-react-devkit/core'`) is the generic UI toolkit. Buttons, inputs, dialogs, tabs, charts, sidebars, toasts, tables. Nothing in it knows what a miner is. It owns colours, fonts, spacing, and the shared styling recipes. **[`/foundation`](/reference/ui/components)** (`import … from '@tetherto/mdk-react-devkit/foundation'`) is built on top of core. It knows about miners, containers, pools, hashrate, alarms, operators, permissions, and settings dashboards. Domain hooks and connected components expect `` to be in place. ## Next steps - Explore the [Component reference](/reference/ui/components): `@tetherto/mdk-react-devkit/core` primitives and `/foundation` mining-domain components - See the [UI Devkit overview](/reference/ui) for how the headless layer fits other frameworks - Browse the [Hooks](/reference/ui/hooks) reference — state, component, and utility hooks # Build a dashboard with an agent (/tutorials/ui/react/build-any-dashboard-with-an-agent) This tutorial walks a **realistic agent session** end to end: the same two-step flow as [Build dashboards with your AI agent](/quickstart/connect-agents). ## Overview MDK's React UI Devkit's library of presentational and composable building blocks can be used wherever suits you. Mining is one of many disciplines that benefits from charts, tables, tabs, and stat cards. Your telemetry; your dash. You bring *your* labels, *your* mock data, and *your* layout. The first thing we built was [Mining Dashboards](/tutorials/ui/react). What will you build? IoT fleet backend reporting, workout metrics motivation app, weather stats? Your data; your choice. The prompt asked for a **statistics tutorial page** so students can explore distributions, trends, and raw grades. You see every: - [UI CLI](/guides/ui/ui-cli) command the agent would run - Resulting page code - Run instructions We use mock JSON in the browser. No Kernel, Worker, pool API, or fleet backend is required. Presentational imports from `@tetherto/mdk-react-devkit/core` are enough for a highly visual, shippable UI. ## What you'll learn 1. **Domain is yours.** Component contracts describe shape and behavior (chart datasets, table columns), not industry vocabulary. 2. **The agent path is unchanged.** `init` → plain-language intent → local manifests → scaffold → `check`. 3. **Visual density is supported.** Bar charts, line trends, sortable tables, and stat cards compose into a dashboard that *looks* like a product, not a wireframe. The demo app is **Stats Lab**: a fictional intro statistics course where Teaching Assistants review quiz score histograms, weekly class averages, and per-student grades. ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ - A project folder inside the MDK UI monorepo (this walkthrough uses `apps/stats-lab`, same pattern as [Wire a React app](/tutorials/ui/react/tutorial#create-your-app)) - [Build dashboards with your AI agent](/quickstart/connect-agents) wired once (`mdk-ui init`) or willingness to run `init` in step 1 ## Agent session walkthrough ### Wire the IDE (same as agents) From your app or monorepo project root: ```bash npx @tetherto/mdk-ui-cli init --ide cursor ``` This writes `.mdk/context.md` and `.cursor/rules/mdk.mdc` so the session already knows the UI CLI surface. See [init](/guides/ui/ui-cli#init). ### State a non-mining intent Paste a prompt that names the domain explicitly so the agent does not reach for hashrate widgets: > Build a **statistics tutorial dashboard** for students in `apps/stats-lab`. Include: > > - A **histogram** of final exam scores (bar chart buckets 50–59 through 90–100) > - A **line chart** of weekly class average over six weeks > - A **sortable table** of students with midterm, final, and section > - Three **summary stat cards**: mean final, median final, enrollment count > > Use mock data in the repo. Import only from `@tetherto/mdk-react-devkit/core` and `@tetherto/mdk-react-devkit/foundation` where > needed. No mining APIs. The agent's job is unchanged from [Build dashboards with your AI agent](/quickstart/connect-agents#prompt-your-agent): discover exports, scaffold, and verify compile. ### Discovery commands the agent runs Behind the prompt, a well-behaved session issues deterministic CLI lookups (no model calls). A representative transcript: ```bash npx @tetherto/mdk-ui-cli suggest "statistics dashboard histogram line chart data table stat cards" ``` Typically returns chart-category components near the top — `BarChart`, `LineChart`, and closely related chart primitives. General-purpose components like `DataTable`, `SingleStatCard`, and `Tabs` score lower on chart-focused queries and are best found with the `find --category` commands in the next accordion. Because we asked for mock data only, the agent skips adapter hooks like `useDevices`. ```bash npx @tetherto/mdk-ui-cli find --category charts --format table npx @tetherto/mdk-ui-cli find --category tables --format table ``` Narrows to chart primitives and table components with stable exports. ```bash npx @tetherto/mdk-ui-cli docs BarChart npx @tetherto/mdk-ui-cli docs LineChart npx @tetherto/mdk-ui-cli example LineChart npx @tetherto/mdk-ui-cli docs DataTable ``` The agent copies real prop shapes (`LineChartData` millisecond `x` values, `DataTableColumnDef` accessors) instead of inventing APIs. ```bash npx @tetherto/mdk-ui-cli add page StatsLab \ --component BarChart \ --component LineChart \ --component DataTable \ --component SingleStatCard npx @tetherto/mdk-ui-cli check apps/stats-lab/src/App.tsx ``` `add page` emits a starter layout; the agent fills in mock datasets and labels. `check` validates imports and component prop contracts against the real package barrels; the app build in the local run verifies the final Vite project. Full command reference: [UI CLI](/guides/ui/ui-cli). ### Review the scaffolded page After the agent edits `App.tsx`, a Stats Lab dashboard might look like this: ```tsx BarChart, LineChart, DataTable, Tabs, TabsList, TabsTrigger, TabsContent, } from '@tetherto/mdk-react-devkit/core' type Student = { id: string name: string section: 'A' | 'B' midterm: number final: number } const students: Student[] = [ { id: '1', name: 'Alex Kim', section: 'A', midterm: 82, final: 88 }, { id: '2', name: 'Jordan Lee', section: 'B', midterm: 74, final: 79 }, { id: '3', name: 'Sam Rivera', section: 'A', midterm: 91, final: 94 }, { id: '4', name: 'Taylor Ng', section: 'B', midterm: 68, final: 72 }, { id: '5', name: 'Casey Park', section: 'A', midterm: 85, final: 90 }, ] const weekStart = (weekIndex: number): number => new Date(2025, 0, 6 + weekIndex * 7).valueOf() const StatsLab = (): React.JSX.Element => { const [sorting, setSorting] = useState([]) const finals = students.map((s) => s.final) const meanFinal = finals.reduce((a, b) => a + b, 0) / finals.length const sortedFinals = [...finals].sort((a, b) => a - b) const medianFinal = sortedFinals[Math.floor(sortedFinals.length / 2)] const histogram = useMemo( () => ({ labels: ['50–59', '60–69', '70–79', '80–89', '90–100'], datasets: [ { label: 'Students', data: [1, 2, 4, 6, 3], backgroundColor: '#6366f1', }, ], }), [], ) const weeklyAverage = useMemo( () => ({ datasets: [ { label: 'Class average', borderColor: '#22c55e', data: [71, 74, 76, 79, 81, 83].map((y, i) => ({ x: weekStart(i), y, })), }, ], }), [], ) const columns: DataTableColumnDef[] = [ { accessorKey: 'name', header: 'Student' }, { accessorKey: 'section', header: 'Section' }, { accessorKey: 'midterm', header: 'Midterm' }, { accessorKey: 'final', header: 'Final' }, ] return (

Stats Lab

Intro statistics — interpret distributions and trends

Charts Roster

Final exam distribution

Weekly class average

) } ```
Nothing in this file references pools, workers, or TH/s. The same components appear on mining pages because the **data model is generic**.
## Run Stats Lab locally Follow the same monorepo workflow as [Wire a React app](/tutorials/ui/react/tutorial). If you already completed that tutorial, skip to **Run the app** with `stats-lab` as the workspace name. ### Clone and build the UI monorepo ```bash git clone https://github.com/tetherto/mdk.git cd mdk ``` ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` ```bash npm install npm run build ``` ### Scaffold `apps/stats-lab` ```bash cd apps npm create vite@latest stats-lab -- --template react-ts cd stats-lab ``` Add workspace dependencies to `package.json`: ```jsonc "@tetherto/mdk-react-devkit": "*", "@tetherto/mdk-react-adapter": "*", "@tetherto/mdk-ui-foundation": "*", ``` Install from the monorepo root: ```bash cd ../.. npm install ``` ### Wrap with `MdkProvider` In `apps/stats-lab/src/main.tsx`, mirror [Wire a React app — Wrap your app in MdkProvider](/tutorials/ui/react/tutorial#wrap-your-app-in-mdkprovider): ```tsx // … ``` Mock data does not call the API; the provider satisfies components that expect React context. ### Run the app From the monorepo root: ```bash npm -w stats-lab run build ``` Then start Vite: ```bash npm -w stats-lab run dev ``` Or from the app folder: ```bash cd apps/stats-lab npm run dev ``` Open the URL Vite prints (typically `http://localhost:5173`). You should see **Stats Lab** with histogram, trend line, stat cards, and a sortable roster tab. ### Optional: compare with the MDK demo app Same commands as [Wire a React app — Run the demo app](/tutorials/ui/react/tutorial#run-the-demo-app): ```bash npm run dev:catalog ``` Open [http://localhost:5173/mdk](http://localhost:5173/mdk) to browse mining-oriented examples, then contrast with Stats Lab: **same primitives, different story**. ## Why the agent stays accurate Manifests list real exports, `check` catches many invented props, and `docs` / `example` ground the session in shipped contracts. The app build remains the final TypeScript and Vite verification. ## Next steps - [Build dashboards with your AI agent](/quickstart/connect-agents): the two-step entry point - [UI CLI reference](/guides/ui/ui-cli): every command in the discovery transcript - [Chart components](/reference/ui/components/charts): `BarChart`, `LineChart`, and related data shapes - [Data display](/reference/ui/components/display): `DataTable` sorting and pagination - [Wire a React app](/tutorials/ui/react/tutorial): hands-on monorepo setup without an agent # Your first component (/tutorials/ui/react/first-component) This tutorial assists first-time users to understand how to render a single MDK component in a React app as fast as possible. This path skips `` which works for presentational `/core` and `/foundation` imports. For adapter hooks or connected foundation components, continue with [Wire a React app](/tutorials/ui/react/tutorial). ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ ### Clone and build The MDK UI (`mdk/ui`) is an npm workspace. Clone the repository, install at the workspace root, and build all packages before adding an app or running the demo. #### 1.1 Clone the repository ```bash git clone https://github.com/tetherto/mdk.git cd mdk/ui ``` ```bash # Requires an SSH key configured for your GitHub account git clone git@github.com:tetherto/mdk.git cd mdk/ui ``` #### 1.2 Install and build ```bash npm install npm run build ``` This builds `@tetherto/mdk-react-devkit` and the other packages in the `ui/` workspace. ### Create your app #### 2.1 Scaffold a Vite React app Create a new React app in the `apps/` folder: ```bash cd apps npm create vite@latest my-app -- --template react-ts cd my-app ``` Ignore the CLI "Now run" instructions — follow the MDK-specific steps below. #### 2.2 Add MDK to `package.json` Open `package.json` and add the workspace dependency: ```jsonc "@tetherto/mdk-react-devkit": "*", ``` ```jsonc { "name": "my-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@tetherto/mdk-react-devkit": "*", // [!code ++] "react": "^19.2.6", // [!code highlight] "react-dom": "^19.2.6" // [!code highlight] }, } ``` Vite also adds a `devDependencies` block (`eslint`, `typescript`, `vite`, `@types/*`); leave that block as is. #### 2.3 Install from the workspace root ```bash cd ../.. # from apps/my-app back up to the workspace root mdk/ui npm install ``` ### Import styles #### 3.1 Import the MDK stylesheet Import the MDK stylesheet once in your app's entry point (typically `src/main.tsx`): ```tsx ``` ```tsx createRoot(document.getElementById('root')!).render( , ) ``` ### Render your first component > This example replaces `App.tsx` with a single mining metric tile that toggles between a normal and an alarmed state. #### 4.1 Replace `App.tsx` with the demo This pulls a button from `@tetherto/mdk-react-devkit/core` and a domain component (`SingleStatCard`) from `@tetherto/mdk-react-devkit/foundation`: ```tsx function App() { const [overheating, setOverheating] = useState(false) return (

Miner status

) } ``` #### 4.2 Run your app From the workspace root mdk/ui, start the dev server for your app. The `-w` name must match the `name` field in `apps/my-app/package.json` (`my-app` from step 2.1): ```bash npm -w my-app run dev ``` Alternatively, from the app folder: ```bash cd apps/my-app npm run dev ``` You should see a card showing `Inlet temperature 28 °C` with a **Simulate overheat** button below it. Click the button and the card flips into an alarm state: red border, red text, value jumps to `78 °C`, and the whole card pulses. Click **Cool down** to reset. That is `@tetherto/mdk-react-devkit/core` (the `Button`) and `@tetherto/mdk-react-devkit/foundation` (the `SingleStatCard`) working together: generic primitives plus mining-domain components, in one app.
## Next steps - To browse the full kit in one app first, see [Explore the demo](/tutorials/ui/explore-the-demo) - [Wire a React app](/tutorials/ui/react/tutorial): add `` and adapter hooks for connected foundation components - [Quickstart](/quickstart/wire-react): minimum integration reference (install, provider, stores, theming) - [Components](/reference/ui/components): primitives and mining-domain components reference (`@tetherto/mdk-react-devkit/core` and `/foundation`) # Wire a React app (/tutorials/ui/react/tutorial) This tutorial walks you through the full React stack: three workspace packages, ``, and adapter hooks. For a faster path that renders one presentational component without the provider, see [Your first component](/tutorials/ui/react/first-component). ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ ### Clone and build The `mdk/ui` directory is the npm workspace root. Clone the repository, install at the workspace root, and build all packages before adding an app or running the demo. #### 1.1 Clone the repository ```bash git clone https://github.com/tetherto/mdk.git cd mdk/ui ``` ```bash # Requires an SSH key configured for your GitHub account git clone git@github.com:tetherto/mdk.git cd mdk/ui ``` #### 1.2 Install and build ```bash npm install npm run build ``` This builds `@tetherto/mdk-react-devkit` and the other packages in the `ui/` workspace. ### Create your app #### 2.1 Scaffold a Vite React app Create a new React app in the `apps/` folder: ```bash cd apps npm create vite@latest my-app -- --template react-ts cd my-app ``` Ignore the CLI "Now run" instructions — follow the MDK-specific steps below. #### 2.2 Add MDK to `package.json` Open `package.json` and add all three workspace packages: ```jsonc "@tetherto/mdk-react-devkit": "*", "@tetherto/mdk-react-adapter": "*", "@tetherto/mdk-ui-foundation": "*", ``` ```jsonc { "name": "my-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@tetherto/mdk-react-devkit": "*", // [!code ++] "@tetherto/mdk-react-adapter": "*", // [!code ++] "@tetherto/mdk-ui-foundation": "*", // [!code ++] "react": "^19.2.6", // [!code highlight] "react-dom": "^19.2.6" // [!code highlight] }, } ``` Vite also adds a `devDependencies` block (`eslint`, `typescript`, `vite`, `@types/*`); leave that block as is. #### 2.3 Install from the workspace root ```bash cd ../.. # from apps/my-app back up to the workspace root mdk/ui npm install ``` ### Wrap your app in MdkProvider `` wires the headless stores from [`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation) into React and sets up the TanStack `QueryClient`. It is required for adapter hooks and connected foundation components. See also [Quickstart — Wrap your app in MdkProvider](/quickstart/wire-react#wrap-your-app-in-mdkprovider). #### 3.1 Update `main.tsx` ```tsx ``` Wrap ``: ```tsx ``` ```tsx createRoot(document.getElementById('root')!).render( , ) ``` ### Use adapter hooks and render a component Adapter hooks subscribe to Zustand stores and re-render when the selected slice changes. This example uses `useAuth` and `useDevices` from the adapter, then renders a presentational `SingleStatCard` from foundation. #### 4.1 Replace `App.tsx` ```tsx function App() { const { permissions } = useAuth() const { selectedDevices } = useDevices() return (

Operator dashboard

Selected devices: {selectedDevices?.length ?? 0}

Permissions loaded: {permissions ? 'yes' : 'no'}

) } ``` Connected foundation components (device explorers, pool manager, settings dashboards) expect `` and often read multiple stores. Start with presentational imports while you wire your API; swap in connected components as your backend comes online.
### Run your app From the workspace root `mdk/ui`: ```bash npm -w my-app run dev ``` Or from the app folder: ```bash cd apps/my-app npm run dev ``` You should see the operator dashboard with device and permission readouts plus a temperature card. The adapter hooks confirm `` is wired — without it, they would throw.
## Next steps - [Quickstart](/quickstart/wire-react): install reference, store hooks, and theming - [Components](/reference/ui/components): primitives and mining-domain components reference (`@tetherto/mdk-react-devkit/core` and `/foundation`) ### Run the demo app If you are already at the workspace root `mdk/ui`: ```bash npm run dev:catalog ``` If your shell is still in the app folder: ```bash cd ../.. # apps/my-app → apps → workspace root mdk/ui npm run dev:catalog ``` Open [http://localhost:5173/mdk](http://localhost:5173/mdk) to browse examples.