# 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](/reference/app-toolkit/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](/reference/app-toolkit/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/react/foundation/reporting/financial#cost), [`Ebitda`](/reference/ui/react/foundation/reporting/financial#ebitda), [`EnergyBalance`](/reference/ui/react/foundation/reporting/financial#energybalance), [`HashBalance`](/reference/ui/react/foundation/reporting/financial#hashbalance), and [`Hashrate`](/reference/ui/react/foundation/reporting/operational#hashrate) — 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/react/foundation/reporting/financial#timeframecontrols), shared across the financial reporting surfaces. ### `TimeframeWeekFlatContent` ```tsx ``` Renders inside the week selector of [`TimeframeControls`](/reference/ui/react/foundation/reporting/financial#timeframecontrols). ### `TimeframeWeekTreeContent` ```tsx ``` Renders inside the week selector of [`TimeframeControls`](/reference/ui/react/foundation/reporting/financial#timeframecontrols). # Compose Cost layouts (/guides/ui/react/compose-reporting-layouts/cost) @tetherto/mdk-react-devkit/foundation The [`Cost`](/reference/ui/react/foundation/reporting/financial#cost) 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/react/foundation/reporting/financial#cost) composite. ### `CostMetrics` ```tsx ``` Renders inside the [`Cost`](/reference/ui/react/foundation/reporting/financial#cost) composite. ### `AvgAllInCostChart` ```tsx ``` Renders inside the [`Cost`](/reference/ui/react/foundation/reporting/financial#cost) composite. ### `ProductionCostChart` ```tsx ``` Renders inside the [`Cost`](/reference/ui/react/foundation/reporting/financial#cost) composite. ### `OperationsEnergyChart` ```tsx ``` Renders inside the [`Cost`](/reference/ui/react/foundation/reporting/financial#cost) composite. # Compose EBITDA layouts (/guides/ui/react/compose-reporting-layouts/ebitda) @tetherto/mdk-react-devkit/foundation The [`Ebitda`](/reference/ui/react/foundation/reporting/financial#ebitda) 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/react/foundation/reporting/financial#ebitda) composite. ### `EbitdaCharts` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/react/foundation/reporting/financial#ebitda) 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/react/foundation/reporting/financial#ebitda) composite. ### `BitcoinProducedCard` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/react/foundation/reporting/financial#ebitda) composite. ### `BitcoinProducedChart` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/react/foundation/reporting/financial#ebitda) composite. ### `BitcoinProductionCostCard` ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/react/foundation/reporting/financial#ebitda) composite. # Compose Energy balance layouts (/guides/ui/react/compose-reporting-layouts/energy-balance) @tetherto/mdk-react-devkit/foundation The [`EnergyBalance`](/reference/ui/react/foundation/reporting/financial#energybalance) 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/react/foundation/reporting/financial#energybalance). ### `EnergyBalanceRevenueMetrics` ```tsx ``` Renders inside the revenue tab of [`EnergyBalance`](/reference/ui/react/foundation/reporting/financial#energybalance). ### `EnergyBalanceCostCharts` ```tsx ``` Renders inside the cost tab of [`EnergyBalance`](/reference/ui/react/foundation/reporting/financial#energybalance). ### `EnergyBalanceCostMetrics` ```tsx ``` Renders inside the cost tab of [`EnergyBalance`](/reference/ui/react/foundation/reporting/financial#energybalance). ### `EnergyBalancePowerChart` ```tsx ``` Renders inside both tabs of [`EnergyBalance`](/reference/ui/react/foundation/reporting/financial#energybalance). ### `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/react/foundation/reporting/financial#hashbalance) 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/react/foundation/reporting/financial#hashbalance). ### `HashBalanceCostPanel` ```tsx ``` Renders inside the cost tab of [`HashBalance`](/reference/ui/react/foundation/reporting/financial#hashbalance). # Compose Hashrate layouts (/guides/ui/react/compose-reporting-layouts/hashrate) @tetherto/mdk-react-devkit/foundation The [`Hashrate`](/reference/ui/react/foundation/reporting/operational#hashrate) 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/react/foundation/reporting/operational#hashrate). ### `HashrateMinerTypeView` ```tsx ``` Renders inside the Miner Type View tab of [`Hashrate`](/reference/ui/react/foundation/reporting/operational#hashrate). ### `HashrateMiningUnitView` ```tsx ``` Renders inside the Mining Unit View tab of [`Hashrate`](/reference/ui/react/foundation/reporting/operational#hashrate). # 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](/reference/app-toolkit/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` - An 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) An 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 an 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. {/* sync with examples/backend/mdk-e2e/client.js help block */}
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+ {/* npm packages are not yet published to the registry; clone the MDK UI monorepo and use workspace dependencies until publish. */} ## 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/react/core/theme) for design tokens and `@layer` override rules. ## Next steps - [Tutorial](/tutorials/ui/react/tutorial) — full integration walkthrough - [Hooks](/reference/app-toolkit/hooks) — [state hooks](/reference/app-toolkit/hooks/state), [component hooks](/reference/app-toolkit/hooks/components), and [utility hooks](/reference/app-toolkit/hooks/utilities) - [Explore the demo](/tutorials/ui/explore-the-demo) — run the demo browser without adding MDK to your own app yet - [Core](/reference/ui/react/core) — building block reference (`@tetherto/mdk-react-devkit/core`) - [Foundation](/reference/ui/react/foundation) — 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/app-toolkit/hooks)**: complete hook catalog: data, state, component, and utility hooks across the adapter and devkit packages - **[UI CLI](/reference/app-toolkit/ui-cli)**: command reference for the UI CLI (agent registry, scaffold, and verify) ### 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 # Hooks (/reference/app-toolkit/hooks) Complete hook catalog for the MDK App Toolkit, covering hooks from both `@tetherto/mdk-react-adapter` (data, state, utilities) and `@tetherto/mdk-react-devkit` (component hooks). Grouped by what each hook depends on rather than which package ships it — this matches the [Developer entry points](/concepts/stack/app-toolkit#developer-entry-points) model where the adapter and the UI Devkit are siblings, so you can adopt only the layers you need. ## At a glance | Bucket | Page | What it covers | Needs | |---|---|---|---| | Components | [Component hooks](/reference/app-toolkit/hooks/components) | Hooks coupled to MDK styled components or shell layout (notifications, forms, charts, dashboards, filters, widgets, tables, reporting) | `@tetherto/mdk-react-devkit` and (for some) `` | | Data | [Data hooks](/reference/app-toolkit/hooks/data) | Adapter hooks that fetch and shape site, pool, dashboard, chart, and auth data | `` from `@tetherto/mdk-react-adapter` | | State | [State hooks](/reference/app-toolkit/hooks/state) | React-bound views of the headless `@tetherto/mdk-ui-foundation` Zustand stores | `` from `@tetherto/mdk-react-adapter` | | Utilities | [Utility hooks](/reference/app-toolkit/hooks/utilities) | Generic React helpers, mining-domain transforms, permission checks, and TanStack Query re-exports | `@tetherto/mdk-react-adapter` and (for some) `` | ## All hooks ### Components @tetherto/mdk-react-devkit | Sub-group | Hooks | |---|---| | Charts | [`useChartDataCheck`](/reference/app-toolkit/hooks/components#usechartdatacheck), [`useEbitda`](/reference/app-toolkit/hooks/components#useebitda), [`useEnergyBalanceViewModel`](/reference/app-toolkit/hooks/components#useenergybalanceviewmodel) | | Dashboards | [`usePoolConfigs`](/reference/app-toolkit/hooks/components#usepoolconfigs), [`useSiteOverviewDetailsData`](/reference/app-toolkit/hooks/components#usesiteoverviewdetailsdata) | | Filters | [`useReportTimeFrameSelectorState`](/reference/app-toolkit/hooks/components#usereporttimeframeselectorstate), [`useTimeframeControls`](/reference/app-toolkit/hooks/components#usetimeframecontrols) | | Forms | [`useFormField`](/reference/app-toolkit/hooks/components#useformfield), [`useFormReset`](/reference/app-toolkit/hooks/components#useformreset) | | Notifications | [`useNotification`](/reference/app-toolkit/hooks/components#usenotification) | | Reporting | [`useHashrate`](/reference/app-toolkit/hooks/components#usehashrate), [`useEnergyReportSite`](/reference/app-toolkit/hooks/components#useenergyreportsite) | | Shell | [`useHeaderControls`](/reference/app-toolkit/hooks/components#useheadercontrols), [`useSidebarExpandedState`](/reference/app-toolkit/hooks/components#usesidebarexpandedstate), [`useSidebarSectionState`](/reference/app-toolkit/hooks/components#usesidebarsectionstate) | | Tables | [`useGetAvailableDevices`](/reference/app-toolkit/hooks/components#usegetavailabledevices) | | Widgets | [`useFinancialDateRange`](/reference/app-toolkit/hooks/components#usefinancialdaterange) | ### Data @tetherto/mdk-react-adapter | Sub-group | Hooks | |---|---| | Auth and token | [`useAuthToken`](/reference/app-toolkit/hooks/data#useauthtoken), [`useTokenPolling`](/reference/app-toolkit/hooks/data#usetokenpolling) | | Chart data | [`useConsumptionChartData`](/reference/app-toolkit/hooks/data#useconsumptionchartdata), [`useHashrateChartData`](/reference/app-toolkit/hooks/data#usehashratechartdata), [`usePowerModeTimelineData`](/reference/app-toolkit/hooks/data#usepowermodetimelinedata) | | Dashboard | [`useDashboardDateRange`](/reference/app-toolkit/hooks/data#usedashboarddaterange), [`useDashboardExport`](/reference/app-toolkit/hooks/data#usedashboardexport), [`useDashboardTimeRange`](/reference/app-toolkit/hooks/data#usedashboardtimerange) | | Incidents | [`useActiveIncidents`](/reference/app-toolkit/hooks/data#useactiveincidents) | | Pool data | [`usePoolRows`](/reference/app-toolkit/hooks/data#usepoolrows), [`usePoolStats`](/reference/app-toolkit/hooks/data#usepoolstats) | | Site data | [`useSiteConsumption`](/reference/app-toolkit/hooks/data#usesiteconsumption), [`useSiteConsumptionChartData`](/reference/app-toolkit/hooks/data#usesiteconsumptionchartdata), [`useSiteContainerCapacity`](/reference/app-toolkit/hooks/data#usesitecontainercapacity), [`useSiteEfficiency`](/reference/app-toolkit/hooks/data#usesiteefficiency), [`useSiteHashrate`](/reference/app-toolkit/hooks/data#usesitehashrate), [`useSiteMinerCounts`](/reference/app-toolkit/hooks/data#usesiteminercounts), [`useSiteMinerStats`](/reference/app-toolkit/hooks/data#usesiteminerstats), [`useSitePowerMeter`](/reference/app-toolkit/hooks/data#usesitepowermeter), [`useSitesOverviewData`](/reference/app-toolkit/hooks/data#usesitesoverviewdata) | ### State @tetherto/mdk-react-adapter | Hook | Summary | |---|---| | [`useAuth`](/reference/app-toolkit/hooks/state#useauth) | React view of the headless `authStore` (token, permissions) | | [`useDevices`](/reference/app-toolkit/hooks/state#usedevices) | React view of the headless `devicesStore` (device list, selection) | | [`useTimezone`](/reference/app-toolkit/hooks/state#usetimezone) | React view of the headless `timezoneStore` (operator timezone) | | [`useNotifications`](/reference/app-toolkit/hooks/state#usenotifications) | React view of the headless `notificationStore` (unread counter) | | [`useActions`](/reference/app-toolkit/hooks/state#useactions) | React view of the headless `actionsStore` (pending submissions) | ### Utilities @tetherto/mdk-react-adapter + @tetherto/mdk-react-devkit/foundation | Sub-group | Hooks | |---|---| | Device and IP | [`usePduViewer`](/reference/app-toolkit/hooks/utilities#usepduviewer) | | Domain transforms | [`useCostSummary`](/reference/app-toolkit/hooks/utilities#usecostsummary), [`useHashBalance`](/reference/app-toolkit/hooks/utilities#usehashbalance), [`useSubsidyFees`](/reference/app-toolkit/hooks/utilities#usesubsidyfees), [`useUpdateExistedActions`](/reference/app-toolkit/hooks/utilities#useupdateexistedactions) | | Generic React | [`useLocalStorage`](/reference/app-toolkit/hooks/utilities#uselocalstorage), [`useKeyDown`](/reference/app-toolkit/hooks/utilities#usekeydown), [`useWindowSize`](/reference/app-toolkit/hooks/utilities#usewindowsize), [`usePlatform`](/reference/app-toolkit/hooks/utilities#useplatform), [`useDeviceResolution`](/reference/app-toolkit/hooks/utilities#usedeviceresolution), [`useBeepSound`](/reference/app-toolkit/hooks/utilities#usebeepsound), [`usePagination`](/reference/app-toolkit/hooks/utilities#usepagination), [`useSubtractedTime`](/reference/app-toolkit/hooks/utilities#usesubtractedtime), [`useTimezoneFormatter`](/reference/app-toolkit/hooks/utilities#usetimezoneformatter) | | Permissions | [`useCheckPerm`](/reference/app-toolkit/hooks/utilities#usecheckperm), [`useHasPerms`](/reference/app-toolkit/hooks/utilities#usehasperms), [`useIsFeatureEditingEnabled`](/reference/app-toolkit/hooks/utilities#useisfeatureeditingenabled) | | TanStack Query re-exports | [Re-exports table](/reference/app-toolkit/hooks/utilities#tanstack-query-re-exports) | ## Imports ```tsx // Data hooks ``` # Component hooks (/reference/app-toolkit/hooks/components) @tetherto/mdk-react-devkit Hooks that wrap or compose styled MDK components — notifications, sidebar/header shell, forms, filters, widgets, tables, charts, and dashboards. Adopt these when you are using `@tetherto/mdk-react-devkit` for your UI. If you bring your own components, you may not need anything here. Start with [State hooks](/reference/app-toolkit/hooks/state) or [Utility hooks](/reference/app-toolkit/hooks/utilities) instead. ## At a glance | Sub-group | Hooks | |---|---| | [Notifications](#notifications) | `useNotification` | | [Shell](#shell) | `useHeaderControls`, `useSidebarExpandedState`, `useSidebarSectionState` | | [Forms](#forms) | `useFormField`, `useFormReset` | | [Filters](#filters) | `useReportTimeFrameSelectorState`, `useTimeframeControls` | | [Widgets](#widgets) | `useFinancialDateRange` | | [Tables](#tables) | `useGetAvailableDevices` | | [Charts](#charts) | `useChartDataCheck`, `useEbitda`, `useEnergyBalanceViewModel` | | [Dashboards](#dashboards) | `usePoolConfigs`, `useSiteOverviewDetailsData` | | [Reporting](#reporting) | `useOperationsDashboard`, `useHashrate`, `useEnergyReportSite` | ## Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/foundation#prerequisites) - For hooks that read from headless stores (`useNotification`, view-model hooks): [wrap your app in ``](/quickstart/wire-react#wrap-your-app-in-mdkprovider) ## Import ```tsx useChartDataCheck, useEbitda, useEnergyBalanceViewModel, useFinancialDateRange, useGetAvailableDevices, useHeaderControls, useNotification, useOperationsDashboard, usePoolConfigs, useReportTimeFrameSelectorState, useSiteOverviewDetailsData, useTimeframeControls, } from '@tetherto/mdk-react-devkit/foundation' useFormField, useFormReset, useSidebarExpandedState, useSidebarSectionState, } from '@tetherto/mdk-react-devkit/core' ``` ## Notifications ### `useNotification` @tetherto/mdk-react-devkit/foundation Show toast notifications backed by the headless `notificationStore`. Supports success, error, info, and warning variants. ```tsx ``` #### Returns | Member | Type | Description | |--------|------|-------------| | `notifySuccess` | `function` | Show success toast | | `notifyError` | `function` | Show error toast | | `notifyInfo` | `function` | Show info toast | | `notifyWarning` | `function` | Show warning toast | #### Method signature ```tsx notifySuccess(message: string, description?: string, options?: NotificationOptions) ``` #### Options Notification methods accept an optional third `options` argument: | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `duration` | Optional | `number` | `3000` | Duration in milliseconds (`0` = no autoclose) | | `position` | Optional | `ToastPosition` | `'top-left'` | Toast position on screen | | `dontClose` | Optional | `boolean` | `false` | When `true`, prevents autoclose | #### Example ```tsx function SaveButton() { const { notifySuccess, notifyError } = useNotification() const handleSave = async () => { try { await saveData() notifySuccess('Saved', 'Your changes have been saved.') } catch (error) { notifyError('Error', 'Failed to save changes.', { dontClose: true }) } } return } ``` ## Shell ### `useHeaderControls` @tetherto/mdk-react-devkit/foundation Read/write hook for the global header-controls store (toggles, sticky flag, theme). ```tsx ``` #### Returns | Member | Type | Description | |--------|------|-------------| | `preferences` | `HeaderPreferences` | Current visibility state for each header item | | `isLoading` | `boolean` | Loading state | | `error` | `Error \| null` | Error state | | `handleToggle` | `function` | Toggle a header item visibility | | `handleReset` | `function` | Reset to default preferences | `handleToggle` and `handleReset` both call `notifySuccess` internally. Every invocation produces a toast notification; avoid calling them in response to fast-changing state. #### Example ```tsx function HeaderSettings() { const { preferences, handleToggle, handleReset } = useHeaderControls() return (
{Object.entries(preferences).map(([key, visible]) => ( handleToggle(key, value)} /> ))}
) } ``` ### `useSidebarExpandedState` @tetherto/mdk-react-devkit/core Persist sidebar expanded/collapsed state in `localStorage` so the layout survives reloads. ```tsx ``` #### Example ```tsx function AppSidebar() { const [expanded, setExpanded] = useSidebarExpandedState(false) return ( ) } ``` ### `useSidebarSectionState` @tetherto/mdk-react-devkit/core Persist individual sidebar section open/closed states in `localStorage`. ```tsx ``` #### Example ```tsx function SidebarSection({ id, title, children }) { const [open, setOpen] = useSidebarSectionState(id, true) return (
{open ? children : null}
) } ``` ## Forms ### `useFormField` @tetherto/mdk-react-devkit/core Read-only context hook for form field children — returns the field's id, error state, and ARIA attributes. ```tsx ``` #### Example ```tsx // Custom component that reads field context — must be rendered inside / function FieldStatusDot() { const { invalid, isDirty } = useFormField() return ( ) } ``` ### `useFormReset` @tetherto/mdk-react-devkit/core Hook to handle form reset with callbacks. ```tsx ``` #### Example ```tsx type MinerFields = { name: string; ip: string } function MinerEditForm({ onSubmit }: { onSubmit: (v: MinerFields) => void }) { const form = useForm({ defaultValues: { name: '', ip: '' } }) const { resetForm, isDirty } = useFormReset({ form, onAfterReset: () => console.log('Form reset'), }) return (
) } ``` ## Filters ### `useReportTimeFrameSelectorState` @tetherto/mdk-react-devkit/foundation State hook backing the reporting time-frame selector — exposes the active window and setters. ```tsx ``` #### Example ```tsx function ReportDateBar() { const { start, end, presetTimeFrame, setPresetTimeFrame } = useReportTimeFrameSelectorState() return (

{start.toLocaleDateString()} – {end.toLocaleDateString()}

) } ``` ### `useTimeframeControls` @tetherto/mdk-react-devkit/foundation Core state machine for TimeframeControls — owns year / month / week selection and resolves the date-range output. ```tsx ``` #### Example ```tsx function YearMonthPicker({ dateRange, onRangeChange, onTimeframeTypeChange }) { const { selectedYear, selectedMonth, handleYearChange, handleMonthTreeChange, yearSelectValue, monthSelectValue, } = useTimeframeControls({ dateRange, timeframeType: null, onRangeChange, onTimeframeTypeChange, isWeekSelectVisible: false, weekTree: false, }) return (
) } ``` ## Widgets ### `useFinancialDateRange` @tetherto/mdk-react-devkit/foundation Resolves the active financial date range (start/end) used by every reporting-section query. ```tsx ``` #### Example ```tsx function ReportingToolbar({ timezone }: { timezone: string }) { const { datePicker, dateRange, onDateRangeReset } = useFinancialDateRange({ timezone }) return (
{datePicker} {dateRange && (

{new Date(dateRange.start).toLocaleDateString()} –{' '} {new Date(dateRange.end).toLocaleDateString()}

)}
) } ``` ## Tables ### `useGetAvailableDevices` @tetherto/mdk-react-devkit/foundation Transforms the host's device list into the available container and miner type sets used by device explorer. Pass `data` from your query result. ```tsx ``` #### Example ```tsx function DeviceTypeFilter({ devices }) { const { availableContainerTypes, availableMinerTypes } = useGetAvailableDevices({ data: devices }) return (
) } ``` ## Charts ### `useChartDataCheck` @tetherto/mdk-react-devkit/foundation Check if chart data is empty or unavailable. Returns `true` if empty (show empty state), `false` if data exists (show chart). ```tsx ``` Pass chart input in one of two shapes: 1. **`dataset`**: direct dataset for BarChart-style usage. 2. **`data`**: Chart.js-shaped object with `datasets` (LineChart) or a `dataset` property. Provide at least one of `dataset` or `data` for a meaningful empty check. #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `dataset` | Optional | `object \| array` | none | Direct dataset for BarChart; set `dataset` or `data` (at least one) for a meaningful check | | `data` | Optional | `object` | none | Chart.js-shaped object with `datasets` (LineChart) or `dataset` property; set `dataset` or `data` (at least one) | #### Returns | Type | Description | |------|-------------| | `boolean` | `true` if data is empty, `false` if data exists | #### Example ```tsx function HashrateChart({ dataset }) { const isEmpty = useChartDataCheck({ dataset }) if (isEmpty) { return } return } ``` ```tsx function TemperatureChart({ data }) { const isEmpty = useChartDataCheck({ data }) return isEmpty ? ( ) : ( ) } ``` #### Chart utility integration `useChartDataCheck` expects **Chart.js-shaped** `data` (`{ labels, datasets }`), not raw `{ labels, series }` from app hooks. Convert hook output with the [**`buildBarChartData` utility**](/reference/ui/react/core/components/charts/composition#chart-utilities) from `@tetherto/mdk-react-devkit/core`, then pass the result to **`useChartDataCheck`**. ```tsx function RevenueBarChart({ hookOutput }) { const chartData = buildBarChartData(hookOutput) const isEmpty = useChartDataCheck({ data: chartData }) return ( ) } ``` For the full `BarChartInput` shape, per-dataset `datalabels` merge, and all-zero empty rules, see [Hook-shaped bar data (`buildBarChartData`)](/reference/ui/react/core/components/charts/composition#hook-shaped-bar-data). ### `useEbitda` @tetherto/mdk-react-devkit/foundation Transforms an `EbitdaResponse` and date-range options into query params and a chart-ready EBITDA view-model. ```tsx ``` #### Example ```tsx // Wire your query result in; consume queryParams to drive the fetch function EbitdaSection({ ebitdaResponse, isLoading, fetchErrors }) { const { datePicker, dateRange, queryParams, errors } = useEbitda({ ebitda: ebitdaResponse, isLoading, fetchErrors, }) // Pass queryParams to your data-fetching layer whenever the date range changes // e.g. useGetEbitdaQuery(queryParams, { skip: !queryParams }) return (
{datePicker} {errors.length > 0 &&

{errors.join(', ')}

}
) } ``` ### `useEnergyBalanceViewModel` @tetherto/mdk-react-devkit/foundation Computes the full EnergyBalance view model from raw API data, managing tab selection and display-mode state. ```tsx ``` #### Example ```tsx function EnergyBalancePanel({ data, isLoading, fetchErrors, dateRange, availablePowerMW }) { const { viewModel, onTabChange, onRevenueDisplayModeChange } = useEnergyBalanceViewModel({ data, isLoading, fetchErrors, dateRange, availablePowerMW, }) return (
{viewModel.isLoading &&

Loading…

} {viewModel.errors.length > 0 &&

{viewModel.errors.join(', ')}

}
) } ``` ## Dashboards ### `usePoolConfigs` @tetherto/mdk-react-devkit/foundation Transforms raw pool-configuration rows from your API into `PoolSummary` objects for the Pool Manager UI. Fetch with TanStack Query in the host app, then pass `data`, `isLoading`, and `error` into this hook. Typical usage: fetch with TanStack Query in the host app, then pass `data`, `isLoading`, and `error` into this hook. Foundation components such as [`PoolManagerPools`](/reference/ui/react/foundation/pool-manager/pools) and [`Miner explorer`](/reference/ui/react/foundation/pool-manager/miner-explorer) expect data shaped this way. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `data` | Optional | `PoolConfigData[]` | none | Raw pool configuration rows from your API | | `isLoading` | Optional | `boolean` | `false` | When `true`, the host should show a loading state | | `error` | Optional | `unknown` | none | Error from your query; surfaced to pool-manager components | #### Returns | Member | Type | Description | |--------|------|-------------| | `pools` | `PoolSummary[]` | Normalized pool list for lists and accordions | | `poolIdMap` | `Record` | Lookup by pool `id` | | `isLoading` | `boolean` | Same as the option you passed in | | `error` | `unknown` | Same as the option you passed in | #### Example ```tsx const { data, isLoading, error } = useGetPoolConfigsQuery({}) return usePoolConfigs({ data, isLoading, error }) } ``` ```tsx function PoolsPage({ poolConfig }: { poolConfig: PoolConfigData[] }) { const { pools, isLoading, error } = usePoolConfigs({ data: poolConfig }) if (isLoading) return if (error) return Failed to load pools return (
    {pools.map((pool) => (
  • {pool.name}
  • ))}
) } ``` ### `useSiteOverviewDetailsData` @tetherto/mdk-react-devkit/foundation Composes the per-site overview view-model: pools, performance series, and recent activity. ```tsx ``` #### Example ```tsx function SiteOverviewCard({ unit, pdus, connectedMiners, isLoading }) { const { containerHashRate, isContainerRunning, minersHashmap, segregatedPduSections, } = useSiteOverviewDetailsData(unit, { pdus, connectedMiners, isLoading }) return (

Hashrate: {containerHashRate}

Status: {isContainerRunning ? 'Running' : 'Offline'}

Miners mapped: {Object.keys(minersHashmap).length}

PDU sections: {Object.keys(segregatedPduSections).join(', ')}

) } ``` ## Reporting ### `useOperationsDashboard` @tetherto/mdk-react-devkit/foundation Shapes raw operational metric logs into chart-ready payloads for the four [`OperationalDashboard`](/reference/ui/react/foundation/reporting/operational#operationaldashboard) cards. It never fetches — wire your data layer (TanStack Query, RTK Query, fixtures) and pass the results in. All unit conversion and series shaping (MH/s → TH/s, W → MW) happens inside the hook so the dashboard components stay purely presentational. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `hashrate` | Optional | `OperationsTrendInput` | none | Hashrate log and loading/error state; `log` entries carry `{ ts, value }` with `value` in MH/s (converted to TH/s internally) | | `consumption` | Optional | `OperationsTrendInput` | none | Power consumption log; `value` in Watts (converted to MW internally) | | `efficiency` | Optional | `OperationsTrendInput` | none | Site efficiency log; `value` in W/TH/s (no conversion) | | `miners` | Optional | `OperationsMinersInput` | none | Per-day miner status counts; `log` entries carry `{ ts, online, error, offline, sleep, maintenance }` | Each `OperationsTrendInput` entry also accepts `nominalValue` (same base unit as `log`) which renders a flat reference line on the chart, and `isLoading` / `error` passed through to the card. #### Returns | Member | Type | Description | |--------|------|-------------| | `hashrate` | `{ data, isLoading, error }` | `LineChartCardData` payload for the hashrate card | | `consumption` | `{ data, isLoading, error }` | `LineChartCardData` payload for the power-consumption card | | `efficiency` | `{ data, isLoading, error }` | `LineChartCardData` payload for the site-efficiency card | | `miners` | `{ data, isLoading, error }` | `{ labels, datasets }` stacked-bar payload for the miners-status card | #### Example ```tsx function OperationsDashboardPage({ hashrateLog, consumptionLog, efficiencyLog, minersLog, isLoading }) { const vm = useOperationsDashboard({ hashrate: { log: hashrateLog, nominalValue: 150_000, isLoading }, consumption: { log: consumptionLog, isLoading }, efficiency: { log: efficiencyLog, isLoading }, miners: { log: minersLog, isLoading }, }) return ( ) } ``` ### `useHashrate` @tetherto/mdk-react-devkit/foundation Base hook for a single Hashrate tab in single-site mode. Normalises a grouped-hashrate query result into the `{ log, isLoading, error }` shape consumed by ``, ``, and ``. Call once per tab — each tab fetches independently because they use different `groupBy` axes. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `query` | Optional | `HashrateQueryState` | none | Result of fetching the v2 `/auth/metrics/hashrate?groupBy=…` endpoint. Wire your data layer (TanStack Query, RTK Query, fixtures) and pass the result here — this hook never fetches directly. | #### Returns | Member | Type | Description | |--------|------|-------------| | `log` | `HashrateGroupedLog \| undefined` | Normalised grouped-hashrate log; `undefined` while loading. | | `isLoading` | `boolean` | Loading state from the upstream query. | | `error` | `unknown` | Error from the upstream query, if any. | #### Example ```tsx function HashrateTab({ groupBy }) { const query = useQuery({ queryKey: ['hashrate', groupBy], queryFn: fetchHashrate }) const { log, isLoading, error } = useHashrate({ query }) return } ``` --- ### `useEnergyReportSite` @tetherto/mdk-react-devkit/foundation Merges site energy consumption data (from the v2 `/auth/metrics/consumption` endpoint) with snapshot tail-log and container list data for the Energy report site tab. Returns the combined view-model consumed by the energy report components. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `dateRange` | Required | `object` | none | **Required.** Active date range `{ start, end }` in ms epoch. | | `consumptionLog` | Required | `object` | none | **Required.** Raw consumption log from the `/auth/metrics/consumption` response. | | `consumptionLoading` | Required | `boolean` | none | **Required.** Loading state of the consumption query. | | `consumptionFetching` | Required | `boolean` | none | **Required.** Background-refetch state of the consumption query. | | `consumptionError` | Required | `unknown` | none | **Required.** Error state of the consumption query. | | `nominalPowerAvailabilityMw` | Required | `number \| undefined` | none | **Required.** Site nominal power capacity in MW from the nominal config. | | `nominalConfigLoading` | Required | `boolean` | none | **Required.** Loading state of the nominal config query. | | `tailLog` | Required | `object` | none | **Required.** Raw tail-log snapshot data. | | `tailLogLoading` | Required | `boolean` | none | **Required.** Loading state of the tail-log query. | | `containers` | Required | `object[]` | none | **Required.** Container list from the device query. | | `containersLoading` | Required | `boolean` | none | **Required.** Loading state of the container query. | #### Returns Returns a `UseEnergyReportSiteResult` object containing the merged power-consumption view-model, power-mode table rows, and combined loading/error states for each data source. Pass directly to the Energy report site-tab components. # Data hooks (/reference/app-toolkit/hooks/data) @tetherto/mdk-react-adapter TanStack Query–backed hooks that fetch live data from the MDK backend and project it into view-model shapes ready for MDK foundation components. These hooks sit in the adapter layer so that tag names, aggregate field keys, and unit conversions stay out of the devkit component layer. All hooks require [``](/quickstart/wire-react#wrap-your-app-in-mdkprovider) to be mounted in the tree. ## At a glance | Sub-group | Hooks | |---|---| | [Alerts](#alerts) | `useCurrentAlertDevices`, `useHistoricalAlerts` | | [Auth and token](#auth-and-token) | `useAuthToken`, `useTokenPolling` | | [Chart data](#chart-data) | `useConsumptionChartData`, `useHashrateChartData`, `usePowerModeTimelineData` | | [Dashboard](#dashboard) | `useDashboardDateRange`, `useDashboardExport`, `useDashboardTimeRange` | | [Incidents](#incidents) | `useActiveIncidents` | | [Pool data](#pool-data) | `usePoolRows`, `usePoolStats` | | [Site data](#site-data) | `useSiteConsumption`, `useSiteConsumptionChartData`, `useSiteContainerCapacity`, `useSiteEfficiency`, `useSiteHashrate`, `useSiteMinerCounts`, `useSiteMinerStats`, `useSitePowerMeter`, `useSitesOverviewData` | ## Prerequisites - [Wrap your app in ``](/quickstart/wire-react#wrap-your-app-in-mdkprovider) - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/foundation#prerequisites) ## Import ```tsx useActiveIncidents, useAuthToken, useConsumptionChartData, useCurrentAlertDevices, useDashboardDateRange, useDashboardExport, useDashboardTimeRange, useHashrateChartData, useHistoricalAlerts, usePoolRows, usePoolStats, usePowerModeTimelineData, useSiteConsumption, useSiteConsumptionChartData, useSiteContainerCapacity, useSiteEfficiency, useSiteHashrate, useSiteMinerCounts, useSiteMinerStats, useSitePowerMeter, useSitesOverviewData, useTokenPolling, } from '@tetherto/mdk-react-adapter' ``` ## Site data ### `useSitesOverviewData` Projects raw site-overview container rows and pool stats into a ``-ready shape. Each container receives its per-container hashrate in MH/s, an attached pool-stats row, and a `mining` / `offline` status derived from the underlying snapshot. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `units` | Required | `ContainerUnit[]` | none | **Required.** Raw container rows from the site overview query. | | `poolStats` | Required | `ContainerPoolStat[]` | none | **Required.** Per-container pool stat rows keyed by container id. | | `isLoading` | Required | `boolean` | none | **Required.** Combined loading state from the upstream queries. | | `tailLogItem` | Required | `SitesOverviewTailLogItem` | none | **Required.** Latest tail-log row from a `stat-1m` miner query; pass `_head(_head(rawResponse))`. | #### Returns | Member | Type | Description | |--------|------|-------------| | `units` | `ProcessedContainerUnit[]` | Processed containers with `hashrateMhs`, `status`, and `poolStats` attached. | | `isLoading` | `boolean` | Passes through the combined loading state from the options. | #### Example ```tsx function SiteOverview({ rawUnits, rawPoolStats, loading, latestTailLog }) { const { units, isLoading } = useSitesOverviewData({ units: rawUnits, poolStats: rawPoolStats, isLoading: loading, tailLogItem: latestTailLog, }) return } ``` ### `useSiteConsumption` Projects the freshest power sample from the dashboard's existing tail-log query into a scalar MW value for the header stats strip (``). Delegates fetching to [`useSiteConsumptionChartData`](#usesiteconsumptionchartdata). ```tsx ``` #### Options Accepts the same `UseConsumptionChartDataParams` object as [`useSiteConsumptionChartData`](#usesiteconsumptionchartdata). | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'5m'`. | | `start` | Optional | `number` | none | Lower time bound (ms epoch). | | `end` | Optional | `number` | none | Upper time bound (ms epoch). | | `tag` | Optional | `string` | `'t-miner'` | Thing tag. Use `'t-powermeter'` for transformer-level readings. | | `powerAttribute` | Optional | `string` | `'power_w_sum_aggr'` | Aggregate field name to read from the tail-log row. | | `refetchInterval` | Optional | `number` | `60000` | Polling interval in ms. Pass `0` to disable. | #### Returns | Member | Type | Description | |--------|------|-------------| | `valueMw` | `number \| undefined` | Latest aggregate value in MW; `undefined` while loading or with no data. | | `valueW` | `number \| undefined` | Raw backend value in watts. | | `isLoading` | `boolean` | Loading state from the upstream query. | ### `useSiteConsumptionChartData` TanStack Query hook that fetches site consumption tail-log samples and returns a ``-ready `ChartCardData` payload. Applies site-powermeter defaults and converts watts to MW. ```tsx ``` #### Options Same `UseConsumptionChartDataParams` as [`useSiteConsumption`](#usesiteconsumption). #### Returns A TanStack `UseQueryResult` whose `data` field is `ChartCardData` — the object accepted by ``. #### Example ```tsx function ConsumptionChart() { const { data, isLoading } = useSiteConsumptionChartData({ timeline: '5m' }) return } ``` ### `useSiteContainerCapacity` Reads the aggregated nominal miner capacity across all site containers from the 5-minute tail-log aggregate, giving the denominator shown in the header (e.g. the "2,188" in "MOS / 2,188"). ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `refetchInterval` | Optional | `number` | `300000` | Polling interval in ms. Pass `0` to disable. | #### Returns | Member | Type | Description | |--------|------|-------------| | `value` | `number \| undefined` | Total nominal miner slots across all site containers; `undefined` while loading. | | `isLoading` | `boolean` | Loading state. | ### `useSiteEfficiency` Derives site efficiency in W/TH/s by dividing the latest site consumption by the latest hashrate. Both upstream hooks are called internally; pass `powerW` to override the consumption source (e.g. to use a power-meter reading instead). ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `timeline` | Required | `string` | none | **Required.** Stat key suffix forwarded to both upstream hooks. | | `start` | Optional | `number` | none | Lower time bound (ms epoch). | | `end` | Optional | `number` | none | Upper time bound (ms epoch). | | `tag` | Optional | `string` | none | Thing tag override forwarded to the consumption hook. | | `powerAttribute` | Optional | `string` | none | Aggregate field override forwarded to the consumption hook. | | `refetchInterval` | Optional | `number` | none | Polling interval in ms forwarded to both upstream hooks. | | `powerW` | Optional | `number` | none | When provided, skips the internal `useSiteConsumption` call and uses this watts value as the numerator directly. Pair with `useSitePowerMeter().valueW` for meter-level efficiency. | #### Returns | Member | Type | Description | |--------|------|-------------| | `valueWthS` | `number \| undefined` | Watts per TH/s; `undefined` while loading or when hashrate is zero. | | `isLoading` | `boolean` | `true` while either upstream hook is loading. | ### `useSiteHashrate` TanStack Query hook that reads the latest aggregate hashrate from the site tail-log and returns both a PH/s display value and the raw MH/s backend value. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'5m'`. | | `start` | Optional | `number` | none | Lower time bound (ms epoch). | | `end` | Optional | `number` | none | Upper time bound (ms epoch). | | `refetchInterval` | Optional | `number` | `60000` | Polling interval in ms. Pass `0` to disable. | #### Returns | Member | Type | Description | |--------|------|-------------| | `valuePhs` | `number \| undefined` | Latest aggregate value in PH/s; `undefined` while loading or with no data. | | `valueMhs` | `number \| undefined` | Latest aggregate value in MH/s (raw backend unit). | | `isLoading` | `boolean` | Loading state. | ### `useSiteMinerCounts` Polls the device list for miners and aggregates them into online / offline / error counts. Uses the `last.status` field on each device row. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `refetchInterval` | Optional | `number` | none | Polling interval in ms. | #### Returns | Member | Type | Description | |--------|------|-------------| | `total` | `number` | Total miner device count. | | `online` | `number` | Miners with status `online` or `on`. | | `offline` | `number` | Miners not online and not in error. | | `error` | `number` | Miners with status `error` or `alert`. | ### `useSiteMinerStats` Polls the `stat-rtd` realtime aggregate for the live miner stat summary — the MOS total, online/offline breakdown, and active pool worker count shown in the header. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `refetchInterval` | Optional | `number` | none | Polling interval in ms. | #### Returns | Member | Type | Description | |--------|------|-------------| | `mosTotal` | `number` | Count of miners that reported hashrate in the last minute (the MOS denominator). | | `online` | `number` | Miners online or with minor errors (green column). | | `offline` | `number` | Offline or sleeping miners. | | `notMining` | `number` | Miners not currently mining. | | `isLoading` | `boolean` | Loading state. | ### `useSitePowerMeter` Reads the current power-meter reading in watts from the site-level device tagged `t-powermeter`. Returns a MW conversion alongside the raw watts value. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `tag` | Optional | `string` | `'t-powermeter'` | Device tag to filter by. | | `refetchInterval` | Optional | `number` | none | Polling interval in ms. | #### Returns | Member | Type | Description | |--------|------|-------------| | `valueMw` | `number \| undefined` | Latest power-meter reading in MW; `undefined` when no device or no data. | | `valueW` | `number \| undefined` | Raw value in watts. | | `isLoading` | `boolean` | Loading state. | ## Pool data ### `usePoolRows` TanStack Query hook that fetches per-pool stats and transforms them into `PoolRow[]` objects shaped for the pool manager table. Hashrate is normalised from raw H/s to PH/s. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `refetchInterval` | Optional | `number` | `120000` | Polling interval in ms. Pass `0` to disable. | #### Returns A TanStack `UseQueryResult` whose `data` is `PoolRow[]`. | Member | Type | Description | |--------|------|-------------| | `id` | `string` | Stable React key derived from `poolType`. | | `name` | `string` | Display name in Moria style — `minerpool-{poolType}-shelf-0`. | | `poolType` | `string` | Raw pool type string, e.g. `'f2pool'`, `'ocean'`. | | `revenue24hBtc` | `number \| undefined` | 24 h revenue in BTC if reported. | | `hashrateHs` | `number \| undefined` | Hashrate in raw H/s. | | `hashratePhsDisplay` | `number \| undefined` | Hashrate in PH/s for display. | | `details` | `PoolDetail[]` | Array of label/value pairs for the expanded row. | ### `usePoolStats` TanStack Query hook that fetches per-pool stats and aggregates them into site-level totals — total workers, online workers, mismatch count, and aggregate hashrate in PH/s. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `refetchInterval` | Optional | `number` | `120000` | Polling interval in ms. Pass `0` to disable. | #### Returns | Member | Type | Description | |--------|------|-------------| | `total` | `number` | Total miners reported across all configured pools. | | `online` | `number` | Pool-reported active workers. | | `mismatch` | `number` | Difference between configured and active workers. | | `hashratePhs` | `number \| undefined` | Aggregate pool hashrate in PH/s; `undefined` while loading or with no data. | | `hashrateHs` | `number \| undefined` | Aggregate pool hashrate in raw H/s. | | `isLoading` | `boolean` | Loading state. | ## Dashboard ### `useDashboardDateRange` Owns the single source of truth for the dashboard's date-range picker: a `start` / `end` ms-epoch pair with setters and a reset helper that restores the default 24-hour window. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `initial` | Optional | `{ start: number; end: number }` | last 24 h | Initial date range. Defaults to the last 24 hours ending now. | #### Returns | Member | Type | Description | |--------|------|-------------| | `start` | `number` | Lower bound (ms epoch). | | `end` | `number` | Upper bound (ms epoch). | | `setRange` | `function` | Replace the current range. | | `reset` | `function` | Convenience: restore the default 24-hour window ending now. | #### Example ```tsx function DashboardHeader() { const { start, end, setRange, reset } = useDashboardDateRange() return } ``` ### `useDashboardExport` Reads cached query data for hashrate, consumption, incidents, and pool stats from the TanStack Query client and exposes `exportCsv`, `exportJson`, and a unified `export(format)` callable. Does not trigger refetches — the export represents exactly what is currently displayed. ```tsx ``` #### Options Accepts a `UseDashboardExportOptions` object containing `DashboardQueryRange` fields (`timeline`, `start`, `end`, `tag`) needed to reconstruct the query keys. | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `timeline` | Required | `string` | none | **Required.** Stat key suffix used to locate the cached chart queries. | | `start` | Optional | `number` | none | Lower time bound (ms epoch). | | `end` | Optional | `number` | none | Upper time bound (ms epoch). | | `tag` | Optional | `string` | none | Thing tag override. | #### Returns | Member | Type | Description | |--------|------|-------------| | `exportCsv` | `function` | Triggers a browser file-download of the dashboard data as CSV. | | `exportJson` | `function` | Triggers a browser file-download as JSON. | | `export` | `function` | Unified callable — `export('csv')` or `export('json')`. | ### `useDashboardTimeRange` Tiny piece of scoped state for the dashboard's timeline selector. Owns the current `timeline` string and the canonical option list. Chart hooks (`useHashrateChartData`, `useSiteConsumptionChartData`, etc.) consume `timeline` as a prop. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `initial` | Optional | `string` | `'5m'` | Initial timeline value. | | `options` | Optional | `TimelineOption[]` | canonical list | Custom option list; defaults to the canonical set from `getTimelineOptions`. | #### Returns | Member | Type | Description | |--------|------|-------------| | `timeline` | `string` | Currently selected timeline string, e.g. `'5m'`. | | `options` | `TimelineOption[]` | Available options, ready for ``. | | `setTimeline` | `function` | Setter for the current timeline. | #### Example ```tsx function DashboardControls() { const { timeline, options, setTimeline } = useDashboardTimeRange() const { valuePhs } = useSiteHashrate({ timeline }) return ( <> {valuePhs?.toFixed(2)} PH/s ) } ``` ## Chart data ### `useConsumptionChartData` TanStack Query hook returning raw consumption tail-log samples. Most dashboards should use the higher-level [`useSiteConsumptionChartData`](#usesiteconsumptionchartdata), which wraps this hook and returns a ``-ready payload. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'1m'`, `'5m'`. | | `start` | Optional | `number` | none | Lower time bound (ms epoch). | | `end` | Optional | `number` | none | Upper time bound (ms epoch). | | `tag` | Optional | `string` | `'t-miner'` | Thing tag. Use `'t-powermeter'` for transformer-level consumption. | | `powerAttribute` | Optional | `string` | `'power_w_sum_aggr'` | Aggregate field name to read. | | `refetchInterval` | Optional | `number` | `60000` | Polling interval in ms. Pass `0` to disable. | #### Returns A TanStack `UseQueryResult` — raw tail-log matrix before projection. ### `useHashrateChartData` TanStack Query hook combining the site tail-log hashrate series with the per-pool hashrate history into a single `ChartCardData` payload for ``. Handles unit conversion from both MH/s (tail-log) and raw H/s (pool API) into PH/s for display. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'5m'`. | | `start` | Optional | `number` | none | Lower time bound (ms epoch). | | `end` | Optional | `number` | none | Upper time bound (ms epoch). | | `refetchInterval` | Optional | `number` | none | Polling interval in ms. | #### Returns A TanStack `UseQueryResult` whose `data` is `ChartCardData` — ready for ``. ### `usePowerModeTimelineData` TanStack Query hook returning power-mode and status samples shaped for ``. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'1m'`. | | `start` | Optional | `number` | none | Lower time bound (ms epoch). | | `end` | Optional | `number` | none | Upper time bound (ms epoch). | | `tag` | Optional | `string` | none | Thing tag override. | | `refetchInterval` | Optional | `number` | none | Polling interval in ms. | #### Returns A TanStack `UseQueryResult`. ## Auth and token ### `useAuthToken` Reads `?authToken=…` from `window.location.search`, persists it into the headless `authStore`, and strips the parameter from the URL via `history.replaceState` so the token never lingers in the address bar. Router-agnostic by design. Returns the current token from the store so callers can gate navigation on authentication. ```tsx ``` #### Returns | Type | Description | |------|-------------| | `string \| null` | Current auth token from the `authStore`, or `null` when no token is present. | #### Example ```tsx function ProtectedRoute() { const token = useAuthToken() return token ? : } ``` ### `useTokenPolling` Polls the backend token-refresh endpoint at a fixed interval (default 250 s) and writes the new token back into the `authStore`. Fires the optional `onSessionEnded` callback on a 401 or 500, which MDK UI Shell uses to redirect back to `/signin`. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `intervalMs` | Optional | `number` | `250000` | Polling interval in ms. Backend token TTL defaults to 5 min (300 s); 250 s gives comfortable headroom. | | `enabled` | Optional | `boolean` | `true` when token present | Pass `false` to pause polling (e.g. on the sign-in page). | | `onSessionEnded` | Optional | `function` | none | Called on 401 or 500; use to navigate to the sign-in page. | #### Example ```tsx function App() { const navigate = useNavigate() useTokenPolling({ onSessionEnded: () => navigate('/signin') }) return } ``` ## Incidents ### `useActiveIncidents` TanStack Query hook returning the list of currently-firing alerts, shaped for ``. Queries devices that have a non-null `last.alerts` field and maps them via the `mapDevicesToIncidents` utility. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `refetchInterval` | Optional | `number` | `20000` | Polling interval in ms. Matches Moria's production cadence. Pass `0` to disable. | | `formatDate` | Optional | `function` | ISO string | Date formatter applied to the row body timestamp. | #### Returns A TanStack `UseQueryResult`. #### Example ```tsx function IncidentsFeed() { const { data: items = [], isLoading } = useActiveIncidents({ refetchInterval: 20_000 }) return } ``` ## Alerts Data hooks that back the full alerts surface: `useCurrentAlertDevices` feeds the `` / `` table with raw device rows; `useHistoricalAlerts` fetches and shapes the historical-alerts log for ``. Both require [``](/quickstart/wire-react#wrap-your-app-in-mdkprovider) in the tree. See also [`useActiveIncidents`](#useactiveincidents) in the Incidents section, which hits the same `list-things` endpoint but maps results to the dashboard card's `IncidentRow[]` shape. ### `useCurrentAlertDevices` TanStack Query hook returning the raw devices that currently carry one or more alerts, as the nested `ListThingsDevice[][]` envelope the devkit `` / `` table expects. Unlike `useActiveIncidents`, this hook leaves the payload unshaped so the table can derive its own filter tokens and per-row status. Both hit `/auth/list-things`; this hook requests a wider field set and uses a distinct cache key. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `refetchInterval` | Optional | `number` | `20000` | Polling interval in ms. Matches the default production polling cadence. Pass `0` to disable. | | `filterTags` | Optional | `string[]` | none | Alerts search chips. Folded into the backend `list-things` selector — triggers a re-fetch that narrows the dataset server-side. | #### Returns A TanStack `UseQueryResult`. #### Example ```tsx function AlertsTable({ filterTags }) { const { data, isLoading } = useCurrentAlertDevices({ filterTags }) return } ``` ### `useHistoricalAlerts` TanStack Query hook for the historical-alerts log. Fetches the `[start, end]` range as successive `history-log` windows, merges results by `uuid`, and shapes rows for the devkit `` table via `mapHistoryLogToAlerts`. Range changes abort the in-flight chunk loop through the query's `AbortSignal`. By default the entire range is fetched in a single request. Pass a smaller `intervalMs` (e.g. `ONE_DAY_MS`) only for wide ranges where the backend would otherwise cap or slow a single query — that splits the fetch into sequential 24-hour windows. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `start` | Required | `number` | none | Lower bound of the look-back window (ms epoch). | | `end` | Required | `number` | none | Upper bound of the look-back window (ms epoch). | | `intervalMs` | Optional | `number` | whole range | Per-request window size in ms. Defaults to the whole range in one request; set a smaller value to chunk wide ranges into sequential fetches. | | `enabled` | Optional | `boolean` | `true` when range valid | Pass `false` to force-disable the query. Defaults to enabled when `start` and `end` are finite and `end > start`. | #### Returns A TanStack `UseQueryResult`. #### Example ```tsx function AlertHistory({ start, end, filterTags, localFilters, onDateRangeChange }) { const { data: alerts = [], isLoading } = useHistoricalAlerts({ start, end }) return ( ) } ``` #### Behavior notes - Wide ranges fan out into many requests. The MDK UI Shell page caps the default window at 14 days; pass a tighter `[start, end]` or set `intervalMs` to limit per-request payload size. - The hook removes duplicate rows by `uuid` after merging chunks, before passing them to `mapHistoryLogToAlerts`. # State hooks (/reference/app-toolkit/hooks/state) @tetherto/mdk-react-adapter State hooks bind the framework-agnostic [`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation) Zustand stores into React. Each hook subscribes the component to its store and re-renders only when the selected slice changes. Use these when you want headless state with your own components. The [Developer entry points](/concepts/stack/app-toolkit#developer-entry-points) table compares adoption layers. ## Prerequisites - [Wrap your app in ``](/quickstart/wire-react#wrap-your-app-in-mdkprovider) - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/foundation#prerequisites) ## Import ```tsx useActions, useAuth, useDevices, useNotifications, useTimezone, } from '@tetherto/mdk-react-adapter' ``` ## `useAuth` @tetherto/mdk-react-adapter React-bound view of the headless [`authStore`](/reference/app-toolkit/ui-foundation). Exposes `token` and `permissions`; equivalent to `useStore(authStore)` with React subscription semantics. ```tsx ``` ### Example ```tsx function SessionStatus() { const { token } = useAuth() if (!token) return

Sign in to continue

return

Active session

} ``` ## `useDevices` @tetherto/mdk-react-adapter React-bound view of the headless [`devicesStore`](/reference/app-toolkit/ui-foundation). Exposes the device list, the currently selected devices, and helpers to mutate selection. ```tsx ``` ### Example ```tsx function DeviceToolbar() { const { selectedDevices, setSelectedDevices } = useDevices() return (

Selected: {selectedDevices.length}

) } ``` ## `useTimezone` @tetherto/mdk-react-adapter React-bound view of the headless [`timezoneStore`](/reference/app-toolkit/ui-foundation). Read or update the operator's timezone preference (IANA identifier). Use [`useTimezoneFormatter`](/reference/app-toolkit/hooks/utilities#usetimezoneformatter) when you also need date-formatting helpers. ```tsx ``` ### Example ```tsx function TimezonePicker() { const { timezone, setTimezone } = useTimezone() return ( ) } ``` ## `useNotifications` @tetherto/mdk-react-adapter React-bound view of the headless [`notificationStore`](/reference/app-toolkit/ui-foundation). Exposes the unread counter (`count`) plus `increment`, `decrement`, and `reset`. For the user-facing toast surface use [`useNotification`](/reference/app-toolkit/hooks/components#usenotification) from foundation. ```tsx ``` ### Example ```tsx function UnreadBadge() { const { count } = useNotifications() return } ``` ## `useActions` @tetherto/mdk-react-adapter React-bound view of the headless [`actionsStore`](/reference/app-toolkit/ui-foundation). Exposes the pending operator submission queue plus helpers like `setAddPendingSubmissionAction` and `removePendingSubmissionAction`. ```tsx ``` ### Example ```tsx function SubmissionTracker() { const { pendingSubmissions, setAddPendingSubmissionAction } = useActions() return (

{pendingSubmissions.length} pending submissions

) } ``` # Utility hooks (/reference/app-toolkit/hooks/utilities) @tetherto/mdk-react-adapter + @tetherto/mdk-react-devkit/foundation Generic React helpers, mining-domain transforms, permission checks, device/IP utilities, and a curated set of TanStack Query re-exports. Use these alongside [State hooks](/reference/app-toolkit/hooks/state) to wire app concerns (permissions, devices, viewport, formatting) without depending on MDK styled components. ## At a glance | Sub-group | Hooks | |---|---| | [Permissions](#permissions) | `useCheckPerm`, `useHasPerms`, `useIsFeatureEditingEnabled` | | [Generic React](#generic-react) | `useLocalStorage`, `useKeyDown`, `useWindowSize`, `usePlatform`, `useDeviceResolution`, `useBeepSound`, `usePagination`, `useSubtractedTime`, `useTimezoneFormatter` | | [Device and IP](#device-and-ip) | `usePduViewer` | | [Domain transforms](#domain-transforms) | `useCostSummary`, `useHashBalance`, `useSubsidyFees`, `useUpdateExistedActions` | | [TanStack Query re-exports](#tanstack-query-re-exports) | `useQuery`, `useMutation`, `useQueries`, `useInfiniteQuery`, `useIsFetching`, `useIsMutating`, `useQueryClient` | ## Prerequisites - [Wrap your app in ``](/quickstart/wire-react#wrap-your-app-in-mdkprovider) - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/foundation#prerequisites) ## Import ```tsx useBeepSound, useCheckPerm, useDeviceResolution, useHasPerms, useIsFeatureEditingEnabled, useKeyDown, useLocalStorage, usePagination, usePduViewer, usePlatform, useSubtractedTime, useTimezoneFormatter, useWindowSize, } from '@tetherto/mdk-react-adapter' useCostSummary, useHashBalance, useSubsidyFees, useUpdateExistedActions, } from '@tetherto/mdk-react-devkit/foundation' ``` ## Permissions ### `useCheckPerm` @tetherto/mdk-react-adapter Check if the current user has a specific permission. Reads `permissions` from the adapter [`authStore`](/reference/app-toolkit/hooks/state#useauth). Prefer this over `useHasPerms` for single-permission gates. ```tsx ``` #### Example ```tsx function EditDevicesButton() { const canEdit = useCheckPerm('devices:edit') return canEdit ? : null } ``` ### `useHasPerms` @tetherto/mdk-react-adapter Returns a callable that accepts a permission request — a string, a string array (first match wins), or a check object. Reads from the adapter [`authStore`](/reference/app-toolkit/hooks/state#useauth). ```tsx ``` #### Example ```tsx function ContextMenu({ device }) { const hasPerms = useHasPerms() return ( {hasPerms(['devices:edit', 'devices:admin']) && Edit} {hasPerms('devices:delete') && Delete} ) } ``` ### `useIsFeatureEditingEnabled` @tetherto/mdk-react-adapter Returns whether the current user has the `features` capability to edit feature flags. ```tsx ``` #### Example ```tsx function FeatureFlagRow({ flag }) { const canEdit = useIsFeatureEditingEnabled() return } ``` ## Generic React ### `useLocalStorage` @tetherto/mdk-react-adapter Type-safe `localStorage` access with cross-tab synchronization. Returns `[value, setValue, removeValue]` and stays in sync across browser tabs via the `storage` event. ```tsx ``` #### Example ```tsx function ThemeToggle() { const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('app:theme', 'light') return } ``` ### `useKeyDown` @tetherto/mdk-react-adapter Track whether a specific keyboard key is currently pressed. Attaches global `keydown`/`keyup` listeners. Useful for modifier-key interactions like shift-click multi-select. ```tsx ``` #### Example ```tsx function MinerGrid({ miners }) { const shiftHeld = useKeyDown('Shift') return miners.map((m) => ( select(m, { range: shiftHeld })} /> )) } ``` ### `useWindowSize` @tetherto/mdk-react-adapter Track window width and height, refreshing on `resize`. Returns `{ windowWidth, windowHeight }`. Use [`useDeviceResolution`](#usedeviceresolution) when you only need a device-class branch rather than raw pixels. ```tsx ``` #### Example ```tsx function ResponsiveChart() { const { windowWidth } = useWindowSize() return } ``` ### `usePlatform` @tetherto/mdk-react-adapter Detect the host platform (iOS, Android, Mac, Windows, Linux) from the user agent. Returns the value matching the exported `OS_TYPES` constant; pair with `detectPlatform` for one-off checks outside React. ```tsx ``` #### Example ```tsx function PlatformBadge() { const platform = usePlatform() return Running on {platform} } ``` ### `useDeviceResolution` @tetherto/mdk-react-adapter Map window width to a device class (`mobile`, `tablet`, `desktop`) using the shared `BREAKPOINTS` constant. Cheaper than re-reading pixels in every render. ```tsx ``` #### Example ```tsx function Layout({ children }) { const device = useDeviceResolution() return device === 'mobile' ? {children} : {children} } ``` ### `useBeepSound` @tetherto/mdk-react-adapter Play a repeating beep when `isAllowed` is true. Configurable volume and interval (`delayMs`). The alarm is synthesised via the Web Audio API — no audio asset is bundled or fetched. Designed for audible critical alerts (overheating containers, equipment failure). ```tsx ``` #### Example ```tsx function CriticalAlertChime({ active }) { useBeepSound({ isAllowed: active, volume: 0.6, delayMs: 1500 }) return null } ``` ### `usePagination` @tetherto/mdk-react-adapter Manage pagination state and produce `{ limit, offset }` query arguments for API calls. Returns state shaped for the devkit `` component plus helpers to change page, page size, and total count. ```tsx ``` #### Options | Option | Status | Type | Default | Description | |--------|--------|------|---------|-------------| | `current` | Optional | `number` | `1` | Initial 1-indexed page | | `pageSize` | Optional | `number` | `20` | Initial rows per page | | `total` | Optional | `number` | none | Initial total row count | | `showSizeChanger` | Optional | `boolean` | `true` | Whether the UI exposes a page-size selector | #### Returns | Member | Type | Description | |--------|------|-------------| | `pagination` | `PaginationState` | `{ current, pageSize, showSizeChanger, total }` — spread onto `` | | `queryArgs` | `{ limit, offset }` | Query arguments for API calls | | `handleChange` | `function` | `(page, pageSize) => void` — pass to `` | | `setPagination` | `function` | Imperative pagination state update | | `reset` | `function` | Reset to initial state | | `setTotal` | `function` | Update total row count | | `hideNextPage` | `function` | Hide next page when the current page has fewer rows than `pageSize` | #### Example ```tsx function MinerList() { const { pagination, queryArgs, handleChange } = usePagination({ current: 1, pageSize: 25 }) const { data } = useQuery(['miners', queryArgs], () => fetchMiners(queryArgs)) return ( <> ) } ``` ### `useSubtractedTime` @tetherto/mdk-react-adapter Returns `Date.now() - diff`, refreshing on a fixed interval (default 5s). Useful for "synced N seconds ago" labels without forcing tree-wide re-renders. ```tsx ``` #### Example ```tsx function LastSyncedLabel({ lastSyncOffsetMs }) { const now = useSubtractedTime(lastSyncOffsetMs) return Synced {formatDistanceToNow(now)} ago } ``` ### `useTimezoneFormatter` @tetherto/mdk-react-adapter Read the app timezone from the adapter [`timezoneStore`](/reference/app-toolkit/hooks/state#usetimezone) and format dates for display. Use when foundation components or app code need timestamps in the operator-selected timezone (alerts, pool manager, dashboard widgets). ```tsx ``` #### Example ```tsx function AlertTimestamp({ raisedAt }) { const { getFormattedDate } = useTimezoneFormatter() return } ``` ## Device and IP ### `usePduViewer` @tetherto/mdk-react-adapter Pan/zoom controller for the PDU floor-plan viewer. Wraps `react-zoom-pan-pinch` with viewport-aware reset logic and a debounced "back to content" indicator that appears when the user pans the layout off-screen. ```tsx ``` #### Example ```tsx function PduFloorPlan() { const { onViewerInit, showBackToContent, handleBackToContent } = usePduViewer() return ( <> {/* …diagram… */} {showBackToContent && } ) } ``` ## Domain transforms ### `useCostSummary` @tetherto/mdk-react-devkit/foundation Base hook for the cost-summary reporting page (single-site mode). ```tsx ``` #### Example ```tsx // Wire your query result in; consume queryParams to drive the fetch function CostSummaryPage({ query }) { const { datePicker, queryParams, isLoading, error } = useCostSummary({ query }) // Pass queryParams to your data-fetching layer whenever the date range changes // e.g. useGetCostSummaryQuery(queryParams, { skip: !queryParams }) return (
{datePicker} {isLoading &&

Loading…

} {error &&

Failed to load cost data

}
) } ``` ### `useHashBalance` @tetherto/mdk-react-devkit/foundation 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. ```tsx ``` #### Example ```tsx function HashBalancePanel({ data, log, currency, dateRange }) { const { siteHashRevenueChartData, networkHashpriceChartData, combinedCostChartData, } = useHashBalance({ data, log, currency, dateRange }) return (
{/* Pass chart datasets to your BarChart components */}
{JSON.stringify(siteHashRevenueChartData?.labels, null, 2)}
{JSON.stringify(networkHashpriceChartData?.labels, null, 2)}
{JSON.stringify(combinedCostChartData?.labels, null, 2)}
) } ``` ### `useSubsidyFees` @tetherto/mdk-react-devkit/foundation 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 `Subsid… ```tsx ``` #### Example ```tsx function SubsidyFeesPanel({ data, dateRange }) { const { summary, subsidyFeesChartData, averageFeesChartData, isEmpty } = useSubsidyFees({ data, dateRange, }) if (isEmpty) return

No subsidy fee data for this period.

return (

Total fees: {(summary as any)?.total ?? '—'}

{/* Pass chart datasets to your BarChart components */}
{JSON.stringify(subsidyFeesChartData?.labels, null, 2)}
{JSON.stringify(averageFeesChartData?.labels, null, 2)}
) } ``` ### `useUpdateExistedActions` @tetherto/mdk-react-devkit/foundation Mutation hook that updates only the changed fields of an existing action record. ```tsx ``` #### Example ```tsx function DeviceActionBar({ actionType, pendingSubmissions, selectedDevices }) { const { updateExistedActions } = useUpdateExistedActions() const handleApply = () => { updateExistedActions({ actionType, pendingSubmissions, selectedDevices }) } return ( ) } ``` ## TanStack Query re-exports The adapter re-exports a curated set of [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) hooks so you can import data-fetching primitives from a single package alongside MDK helpers. The re-exports are unmodified — refer to the upstream TanStack Query documentation for full API details. | Hook | TanStack docs | | --- | --- | | `useQuery` | [TanStack `useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) | | `useMutation` | [TanStack `useMutation`](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) | | `useQueries` | [TanStack `useQueries`](https://tanstack.com/query/latest/docs/framework/react/reference/useQueries) | | `useInfiniteQuery` | [TanStack `useInfiniteQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery) | | `useIsFetching` | [TanStack `useIsFetching`](https://tanstack.com/query/latest/docs/framework/react/reference/useIsFetching) | | `useIsMutating` | [TanStack `useIsMutating`](https://tanstack.com/query/latest/docs/framework/react/reference/useIsMutating) | | `useQueryClient` | [TanStack `useQueryClient`](https://tanstack.com/query/latest/docs/framework/react/reference/useQueryClient) | # UI CLI reference (/reference/app-toolkit/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 # UI Devkit (/reference/app-toolkit/ui-devkit) The UI Devkit is the frontend tools half of the [MDK App Toolkit](/concepts/stack/app-toolkit). This reference documents its constants, React hooks, TypeScript types, and helper utilities. ## Browse by topic | Topic | What's there | |-------|--------------| | [Constants](/reference/app-toolkit/ui-devkit/constants) | Colors, units, currency, chart configs and permissions, roles, header preferences| | [Hooks](/reference/app-toolkit/ui-devkit/hooks) | Discover hooks for monitoring and UI patterns; full reference at [Hooks](/reference/app-toolkit/hooks) | | [Types](/reference/app-toolkit/ui-devkit/types) | TypeScript type exports. UI primitives and domain models like `Device`, `Alert` | | [Utilities](/reference/app-toolkit/ui-devkit/utilities) | Helper functions for formatting, validation, conversions, and settings persistence | ## Browse by package If you're working with a specific package, use these per-package shortcuts: ### `@tetherto/mdk-react-devkit/core` - [Constants](/reference/app-toolkit/ui-devkit/constants#core-constants) - [Types](/reference/app-toolkit/ui-devkit/types#core-types) - [Utilities](/reference/app-toolkit/ui-devkit/utilities#core-utilities) ### `@tetherto/mdk-react-devkit/foundation` - [Constants](/reference/app-toolkit/ui-devkit/constants#foundation-constants) - [Types](/reference/app-toolkit/ui-devkit/types#foundation-types) - [Utilities](/reference/app-toolkit/ui-devkit/utilities#foundation-utilities) - [Hooks](/reference/app-toolkit/hooks) — every App Toolkit hook (state, components, utilities) lives on a single overview page now. ### `@tetherto/mdk-ui-foundation` - [Alert utilities](/reference/app-toolkit/ui-devkit/utilities#ui-foundation-alert-utilities) — alert query-parameter builders and time-range chunking helpers # Constants (/reference/app-toolkit/ui-devkit/constants) This page documents constants exported by the MDK packages. Constants live in two distinct domains: - [Core constants](#core-constants) ships UI primitives: colors, units, currency symbols, chart defaults - [Foundation constants](#foundation-constants) ships mining-domain values: permissions, roles, header preferences, error codes ## Core constants UI primitive constants exported by `@tetherto/mdk-react-devkit/core` — colors, units, currency symbols, and chart defaults. ### Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/core#prerequisites) ### Import @tetherto/mdk-react-devkit/core ```tsx COLOR, UNITS, CURRENCY, CHART_COLORS, TABLE_COLORS, HASHRATE_LABEL_DIVISOR, } from '@tetherto/mdk-react-devkit/core' ``` ### Color constants #### `COLOR` @tetherto/mdk-react-devkit/core Comprehensive color palette with 80+ named colors. ```tsx COLOR.GREEN // '#72F59E' COLOR.RED // '#EF4444' COLOR.COLD_ORANGE // '#F7931A' ``` ##### Base colors | Constant | Value | Description | |----------|-------|-------------| | `WHITE` | `#FFFFFF` | Pure white | | `BLACK` | `#17130F` | Standard black | | `DARK_BACK` | `#1A1815` | Dark background | | `EBONY` | `#0f0f0f` | Chart background | | `TRANSPARENT` | `transparent` | Transparent | ##### Status colors | Constant | Value | Description | |----------|-------|-------------| | `GREEN` | `#72F59E` | Success/online | | `RED` | `#EF4444` | Error/danger | | `YELLOW` | `#FFC107` | Warning | | `BRIGHT_YELLOW` | `#EAB308` | Bright warning | | `LIGHT_BLUE` | `#22AFFF` | Info | | `SLEEP_BLUE` | `#3B82F6` | Sleep/standby | ##### Brand colors | Constant | Value | Description | |----------|-------|-------------| | `COLD_ORANGE` | `#F7931A` | Bitcoin orange | | `ORANGE` | `#FF6A00` | Primary orange | | `EMERALD` | `#009393` | Teal accent | | `INDIGO` | `#5B5FFB` | Purple accent | #### `TABLE_COLORS` @tetherto/mdk-react-devkit/core Colors for table styling. ```tsx TABLE_COLORS.HEADER_BG TABLE_COLORS.ROW_HOVER ``` #### `HEATMAP` @tetherto/mdk-react-devkit/core `HEATMAP` color scale for temperature and intensity displays. ```tsx ``` #### `CHART_COLORS` @tetherto/mdk-react-devkit/core Default chart color palette. ```tsx ``` #### `PIE_CHART_COLORS` @tetherto/mdk-react-devkit/core Color palette for pie and doughnut charts. ```tsx ``` #### `CATEGORICAL_COLORS` @tetherto/mdk-react-devkit/core 25-color categorical palette for multi-series charts. ```tsx CATEGORICAL_COLORS[0] // First color CATEGORICAL_COLORS[24] // Last color ``` #### `TEMPERATURE_COLORS` @tetherto/mdk-react-devkit/core Color scale for temperature displays. ```tsx ``` #### `SOCKET_BORDER_COLOR` @tetherto/mdk-react-devkit/core Colors for socket status indicators. ```tsx ``` ### Unit constants #### `UNITS` @tetherto/mdk-react-devkit/core Physical and measurement units. ```tsx UNITS.POWER_W // 'W' UNITS.POWER_KW // 'kW' UNITS.ENERGY_MWH // 'MWh' UNITS.TEMPERATURE_C // '°C' UNITS.HASHRATE_TH_S // 'TH/s' ``` | Constant | Value | Description | |----------|-------|-------------| | `POWER_W` | `W` | Watts | | `POWER_KW` | `kW` | Kilowatts | | `ENERGY_WH` | `Wh` | Watt-hours | | `ENERGY_KWH` | `kWh` | Kilowatt-hours | | `ENERGY_MW` | `MW` | Megawatts | | `ENERGY_MWH` | `MWh` | Megawatt-hours | | `ENERGY_GWH` | `GWh` | Gigawatt-hours | | `TEMPERATURE_C` | `°C` | Celsius | | `VOLTAGE_V` | `V` | Volts | | `AMPERE` | `A` | Amperes | | `PERCENT` | `%` | Percentage | | `PRESSURE_BAR` | `bar` | Pressure (bar) | | `HASHRATE_MH_S` | `MH/s` | Megahash/second | | `HASHRATE_TH_S` | `TH/s` | Terahash/second | | `HASHRATE_PH_S` | `PH/s` | Petahash/second | | `HASHRATE_EH_S` | `EH/s` | Exahash/second | | `FREQUENCY_MHZ` | `MHz` | Megahertz | | `FREQUENCY_HERTZ` | `Hz` | Hertz | | `HUMIDITY_PERCENT` | `%RH` | Relative humidity | | `EFFICIENCY_W_PER_TH` | `W/TH` | Watts per terahash | | `FLOW_M3H` | `m3/h` | Flow rate | | `SATS` | `Sats` | Satoshis | | `VBYTE` | `vByte` | Virtual bytes | #### `CURRENCY` @tetherto/mdk-react-devkit/core Currency symbols and labels. ```tsx CURRENCY.BTC // '₿' CURRENCY.USD // '$' CURRENCY.EUR // '€' CURRENCY.SATS // 'Sats' CURRENCY.BTC_LABEL // 'BTC' CURRENCY.USD_LABEL // 'USD' ``` #### `MAX_UNIT_VALUE` @tetherto/mdk-react-devkit/core Maximum values for certain units. ```tsx MAX_UNIT_VALUE.HUMIDITY_PERCENT // 100 MAX_UNIT_VALUE.TEMPERATURE_PERCENT // 100 ``` #### `HASHRATE_LABEL_DIVISOR` @tetherto/mdk-react-devkit/core Divisors for converting hashrate units. ```tsx HASHRATE_LABEL_DIVISOR['TH/s'] // 1e6 HASHRATE_LABEL_DIVISOR['PH/s'] // 1e9 HASHRATE_LABEL_DIVISOR['EH/s'] // 1e12 ``` ### Chart constants #### `defaultChartColors` @tetherto/mdk-react-devkit/core Default color array for chart datasets. ```tsx ``` #### `defaultChartOptions` @tetherto/mdk-react-devkit/core Default [Chart.js](https://www.chartjs.org/) options. ```tsx ``` #### `CHART_LEGEND_OPACITY` @tetherto/mdk-react-devkit/core Opacity values for chart legends. ```tsx ``` #### `CHART_PERFORMANCE` @tetherto/mdk-react-devkit/core Performance threshold constants for charts. ```tsx ``` #### `getChartAnimationConfig` @tetherto/mdk-react-devkit/core Get animation configuration based on data count. ```tsx const animConfig = getChartAnimationConfig(dataPointCount) ``` #### `getDataDecimationConfig` @tetherto/mdk-react-devkit/core Get data decimation configuration for large datasets. ```tsx const decimationConfig = getDataDecimationConfig(dataPointCount) ``` ### Type exports The constants module also exports TypeScript types: ```tsx UnitKey, UnitValue, CurrencyKey, CurrencyValue, } from '@tetherto/mdk-react-devkit/core' ``` ## Foundation constants Constants exported by `@tetherto/mdk-react-devkit/foundation`: app identity, dialog flows, header preferences, permissions, roles, and error codes. ### Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/foundation#prerequisites) ### Import @tetherto/mdk-react-devkit/foundation ```tsx WEBAPP_NAME, WEBAPP_SHORT_NAME, WEBAPP_DISPLAY_NAME, POSITION_CHANGE_DIALOG_FLOWS, HEADER_ITEMS, DEFAULT_HEADER_PREFERENCES, AUTH_PERMISSIONS, AUTH_LEVELS, USER_ROLE, USER_ROLES, PERM_LEVEL_LABELS, getRoleBadgeColors, } from '@tetherto/mdk-react-devkit/foundation' ``` ### App identity Brand strings the foundation UI uses to label the running web app in header rows, chart legends, settings export errors, and confirmation copy. The kit ships placeholder defaults; consumers read them via the named imports. #### `WEBAPP_NAME` @tetherto/mdk-react-devkit/foundation Inline name used in confirmations and the `parseSettingsFile` import-error message. ```tsx WEBAPP_NAME // 'Appl.' ``` #### `WEBAPP_SHORT_NAME` @tetherto/mdk-react-devkit/foundation Compact label embedded in [`HEADER_ITEMS`](#header_items) for the in-app miner and hashrate header rows. ```tsx WEBAPP_SHORT_NAME // 'APP' ``` #### `WEBAPP_DISPLAY_NAME` @tetherto/mdk-react-devkit/foundation Display name used in chart legends, including the hash-rate line chart series label. ```tsx WEBAPP_DISPLAY_NAME // 'Application' ``` ### Dialog flows #### `POSITION_CHANGE_DIALOG_FLOWS` @tetherto/mdk-react-devkit/foundation Flow keys consumed by [`PositionChangeDialog`](/reference/ui/react/foundation/operations/details-view/fleet-management#positionchangedialog) and its sibling dialogs to route between step-specific surfaces. ```tsx POSITION_CHANGE_DIALOG_FLOWS.MAINTENANCE // 'maintenance' POSITION_CHANGE_DIALOG_FLOWS.REPLACE_MINER // 'replaceMiner' ``` | Constant | Value | Description | |----------|-------|-------------| | `CONFIRM_REMOVE` | `remove` | Render the remove-miner confirmation surface | | `CHANGE_INFO` | `changeInfo` | Edit info for an existing miner | | `MAINTENANCE` | `maintenance` | Move a miner to (or back from) the maintenance container | | `REPLACE_MINER` | `replaceMiner` | Replace the miner currently occupying a socket | | `CONFIRM_CHANGE_POSITION` | `confirmChange` | Confirm a position change between sockets | | `CONTAINER_SELECTION` | `containerSelection` | Pick a destination container and socket | #### `PositionChangeDialogFlowKey` type @tetherto/mdk-react-devkit/foundation ```tsx type PositionChangeDialogFlowKey = keyof typeof POSITION_CHANGE_DIALOG_FLOWS ``` #### `PositionChangeDialogFlowValue` type @tetherto/mdk-react-devkit/foundation ```tsx type PositionChangeDialogFlowValue = (typeof POSITION_CHANGE_DIALOG_FLOWS)[PositionChangeDialogFlowKey] ``` ### Header controls #### `HEADER_ITEMS` @tetherto/mdk-react-devkit/foundation Array of header metric options for the [`HeaderControlsSettings`](/reference/ui/react/foundation/settings/header-controls) component. ```tsx ``` | Key | Label | |-----|-------| | `poolMiners` | `Pool Miners` | | `miners` | `` `${WEBAPP_SHORT_NAME} Miners` `` | | `poolHashrate` | `Pool Hashrate` | | `hashrate` | `` `${WEBAPP_SHORT_NAME} Hashrate` `` | | `consumption` | `Consumption` | | `efficiency` | `Efficiency` | The two in-app rows compose their labels from [`WEBAPP_SHORT_NAME`](#webapp_short_name); with the shipped default that renders as `APP Miners` and `APP Hashrate`. #### `DEFAULT_HEADER_PREFERENCES` @tetherto/mdk-react-devkit/foundation Default visibility state for all header items (all `true`). ```tsx DEFAULT_HEADER_PREFERENCES.poolMiners // true DEFAULT_HEADER_PREFERENCES.consumption // true ``` #### `HeaderPreferences` type @tetherto/mdk-react-devkit/foundation ```tsx type HeaderPreferences = { poolMiners: boolean miners: boolean poolHashrate: boolean hashrate: boolean consumption: boolean efficiency: boolean } ``` ### Permissions #### `AUTH_PERMISSIONS` @tetherto/mdk-react-devkit/foundation Permission resource identifiers. ```tsx AUTH_PERMISSIONS.USERS // 'users' AUTH_PERMISSIONS.SETTINGS // 'settings' AUTH_PERMISSIONS.MINER // 'miner' ``` | Constant | Value | Description | |----------|-------|-------------| | `USERS` | `users` | User management | | `SETTINGS` | `settings` | Application settings | | `MINER` | `miner` | Miner operations | | `ALERTS` | `alerts` | Alert management | | `ACTIONS` | `actions` | Action execution | | `EXPLORER` | `explorer` | Device explorer | | `INVENTORY` | `inventory` | Inventory management | | `CONTAINER` | `container` | Container management | | `PRODUCTION` | `production` | Production data | | `REPORTING` | `reporting` | Reports | #### `AUTH_LEVELS` @tetherto/mdk-react-devkit/foundation Permission access levels. ```tsx AUTH_LEVELS.READ // 'r' AUTH_LEVELS.WRITE // 'w' ``` #### `USER_ROLE` @tetherto/mdk-react-devkit/foundation User role identifiers. ```tsx USER_ROLE.ADMIN // 'admin' USER_ROLE.SITE_MANAGER // 'site_manager' USER_ROLE.READ_ONLY // 'read_only_user' ``` | Constant | Value | |----------|-------| | `ADMIN` | `admin` | | `SITE_MANAGER` | `site_manager` | | `SITE_OPERATOR` | `site_operator` | | `FIELD_OPERATOR` | `field_operator` | | `REPAIR_TECHNICIAN` | `repair_technician` | | `REPORTING_TOOL_MANAGER` | `reporting_tool_manager` | | `READ_ONLY` | `read_only_user` | ### Settings #### `USER_ROLES` @tetherto/mdk-react-devkit/foundation Array of role options for select dropdowns. ```tsx // [{ label: 'Admin', value: 'admin' }, ...] ``` #### `PERM_LEVEL_LABELS` @tetherto/mdk-react-devkit/foundation Human-readable labels for permission levels. ```tsx PERM_LEVEL_LABELS.rw // 'Read & Write' PERM_LEVEL_LABELS.r // 'Read Only' PERM_LEVEL_LABELS.none // 'No Access' ``` #### `SETTINGS_ERROR_CODES` @tetherto/mdk-react-devkit/foundation Error code to message mapping. ```tsx SETTINGS_ERROR_CODES.ERR_USER_EXISTS // 'User already exists' SETTINGS_ERROR_CODES.DEFAULT // 'An error occurred' ``` ### Role styling #### `getRoleBadgeColors` @tetherto/mdk-react-devkit/foundation Get badge colors for a role. ```tsx const { color, bgColor } = getRoleBadgeColors('admin') // { color: '#e8833a', bgColor: 'rgba(232, 131, 58, 0.1)' } ``` | Role | Color | Background | |------|-------|------------| | `admin` | `#e8833a` | Orange tint | | `site_manager` | `#52c41a` | Green tint | | `site_operator` | `#faad14` | Yellow tint | | `read_only_user` | `#8c8c8c` | Gray tint | # Hooks (/reference/app-toolkit/ui-devkit/hooks) Hooks shipped by `@tetherto/mdk-react-devkit`. For the full catalog — including adapter data hooks and state hooks from `@tetherto/mdk-react-adapter` — see the [Hooks reference](/reference/app-toolkit/hooks). Use the groups below to browse by concern; each hook name links to its entry in that reference. ## Available hooks | Group | Hooks | |---|---| | [Monitoring](/reference/app-toolkit/hooks) | [`useChartDataCheck`](/reference/app-toolkit/hooks/components#usechartdatacheck), [`useBeepSound`](/reference/app-toolkit/hooks/utilities#usebeepsound), [`useEnergyReportSite`](/reference/app-toolkit/hooks/components#useenergyreportsite), [`useHashrate`](/reference/app-toolkit/hooks/components#usehashrate) | | [UI](/reference/app-toolkit/hooks) | [`useNotification`](/reference/app-toolkit/hooks/components#usenotification), [`useHeaderControls`](/reference/app-toolkit/hooks/components#useheadercontrols), [`useSidebarExpandedState`](/reference/app-toolkit/hooks/components#usesidebarexpandedstate), [`useSidebarSectionState`](/reference/app-toolkit/hooks/components#usesidebarsectionstate), [`useHasPerms`](/reference/app-toolkit/hooks/utilities#usehasperms), [`useCheckPerm`](/reference/app-toolkit/hooks/utilities#usecheckperm), [`useLocalStorage`](/reference/app-toolkit/hooks/utilities#uselocalstorage), [`usePagination`](/reference/app-toolkit/hooks/utilities#usepagination), [`useIsFeatureEditingEnabled`](/reference/app-toolkit/hooks/utilities#useisfeatureeditingenabled) | # Types (/reference/app-toolkit/ui-devkit/types) This page documents the TypeScript types exported by the MDK packages. The two packages cover different territory: - [Core types](#core-types) ships **UI primitive types**: sizes, variants, colors, status, common API and chart shapes. These are the building blocks consumed by core components and re-used in your own component prop types. - [Foundation types](#foundation-types) ships **mining-domain models**: `Device`, `Container`, `Alert`, `MinerStats`, settings shapes. These describe the data flowing through foundation components and the API responses they consume. ## Core types UI primitive types exported by `@tetherto/mdk-react-devkit/core` — sizes, variants, colors, status, and common API and chart shapes. These are the building blocks consumed by core components and re-used in your own component prop types. ### Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/core#prerequisites) ### Import @tetherto/mdk-react-devkit/core ```tsx ComponentSize, ButtonVariant, ColorVariant, Status, ApiResponse, } from '@tetherto/mdk-react-devkit/core' ``` ### Common types #### `ComponentSize` @tetherto/mdk-react-devkit/core Standard size variants used across multiple components. ```tsx type ComponentSize = 'sm' | 'md' | 'lg' ``` Used by: `Button`, `Badge`, `Checkbox`, `Switch`, `Radio`, `Spinner`, `Indicator`, `EmptyState`, `Pagination`. #### `ButtonSize` @tetherto/mdk-react-devkit/core Extends `ComponentSize` with an icon-only variant. ```tsx type ButtonSize = ComponentSize | 'icon' ``` #### `BorderRadius` @tetherto/mdk-react-devkit/core Border radius variants for form components. ```tsx type BorderRadius = 'none' | 'small' | 'medium' | 'large' | 'full' ``` Used by: `Checkbox`, `Switch`, `Radio`. #### `ColorVariant` @tetherto/mdk-react-devkit/core Comprehensive color variants for components. ```tsx type ColorVariant = | 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' ``` #### `StatusVariant` @tetherto/mdk-react-devkit/core Status variants for state indication. ```tsx type StatusVariant = 'success' | 'processing' | 'error' | 'warning' | 'default' | 'idle' ``` Used by: badges, notifications, status indicators. #### `ComponentColor` @tetherto/mdk-react-devkit/core Color options for form components. ```tsx type ComponentColor = 'default' | 'primary' | 'success' | 'warning' | 'error' ``` Used by: `Checkbox`, `Switch`, `Radio`, `Typography`. #### `Position` @tetherto/mdk-react-devkit/core Position/side options for UI elements. ```tsx type Position = 'top' | 'right' | 'bottom' | 'left' ``` Used by: `Tooltip`, `Popover`, chart legends. #### `TextAlign` @tetherto/mdk-react-devkit/core Text alignment options. ```tsx type TextAlign = 'left' | 'center' | 'right' | 'justify' ``` #### `FlexAlign` @tetherto/mdk-react-devkit/core Flex/grid alignment options. ```tsx type FlexAlign = 'start' | 'center' | 'end' ``` ### Component types #### `ButtonVariant` @tetherto/mdk-react-devkit/core Button visual variants. ```tsx type ButtonVariant = | 'primary' | 'secondary' | 'danger' | 'tertiary' | 'link' | 'icon' | 'outline' | 'ghost' ``` #### `ButtonIconPosition` @tetherto/mdk-react-devkit/core Where icons appear in buttons. ```tsx type ButtonIconPosition = 'left' | 'right' ``` #### `NotificationVariant` @tetherto/mdk-react-devkit/core Toast/notification variants. ```tsx type NotificationVariant = 'success' | 'error' | 'warning' | 'info' ``` #### `BadgeStatus` @tetherto/mdk-react-devkit/core Badge status options. ```tsx type BadgeStatus = 'success' | 'processing' | 'error' | 'warning' | 'default' ``` #### `TypographyColor` @tetherto/mdk-react-devkit/core Typography color options including muted. ```tsx type TypographyColor = 'default' | 'primary' | 'success' | 'warning' | 'error' | 'muted' ``` ### Utility types #### `UnknownRecord` @tetherto/mdk-react-devkit/core Generic type for objects with unknown structure. ```tsx type UnknownRecord = Record ``` #### `Nullable` / `Optional` / `Maybe` @tetherto/mdk-react-devkit/core Null/undefined wrapper types. ```tsx type Nullable = T | null type Optional = T | undefined type Maybe = T | null | undefined ``` #### `Status` @tetherto/mdk-react-devkit/core Async operation status. ```tsx type Status = 'idle' | 'loading' | 'success' | 'error' ``` ### API types #### `PaginationParams` @tetherto/mdk-react-devkit/core Pagination request parameters. ```tsx type PaginationParams = { limit?: number offset?: number page?: number } ``` #### `PaginatedResponse` @tetherto/mdk-react-devkit/core Paginated response wrapper. ```tsx type PaginatedResponse = { data: T[] page: number total: number totalPages: number } ``` #### `ApiResponse` @tetherto/mdk-react-devkit/core API response wrapper. ```tsx type ApiResponse = { data: T message?: string status: number } ``` #### `ApiError` @tetherto/mdk-react-devkit/core API error response structure. ```tsx type ApiError = { error: string message: string status: number data?: { message?: string } } ``` ### Data table types @tetherto/mdk-react-devkit/core Re-exported from TanStack Table for convenience. ```tsx DataTableColumnDef, DataTableExpandedState, DataTablePaginationState, DataTableRow, DataTableRowSelectionState, DataTableSortingState, } from '@tetherto/mdk-react-devkit/core' ``` ### Value types #### `ValueUnit` @tetherto/mdk-react-devkit/core A value paired with a unit, used for display formatting. ```tsx type ValueUnit = { value: number | string | null unit: string realValue: number } ``` #### `HashrateUnit` / `CurrencyUnit` @tetherto/mdk-react-devkit/core Specialized value-unit aliases. ```tsx type HashrateUnit = ValueUnit type CurrencyUnit = ValueUnit ``` #### `UnitLabel` @tetherto/mdk-react-devkit/core SI-prefix unit labels. ```tsx type UnitLabel = 'decimal' | 'k' | 'M' | 'G' | 'T' | 'P' ``` ### Time types #### `TimeRangeFormatted` @tetherto/mdk-react-devkit/core A formatted time range. ```tsx type TimeRangeFormatted = { start: string end: string formatted: string } ``` #### `TimeInterval` @tetherto/mdk-react-devkit/core A time interval with start/end timestamps. ```tsx type TimeInterval = { start: number end: number } ``` ### Chart types #### `ChartLegendPosition` @tetherto/mdk-react-devkit/core Chart legend position options. ```tsx type ChartLegendPosition = 'top' | 'bottom' | 'left' | 'right' | 'center' | 'chartArea' ``` #### `WeightedAverageResult` @tetherto/mdk-react-devkit/core Result from weighted average calculation. ```tsx type WeightedAverageResult = { avg: number totalWeight: number weightedValue: number } ``` #### `ErrorWithTimestamp` @tetherto/mdk-react-devkit/core An error with optional timestamp. ```tsx type ErrorWithTimestamp = { msg?: string message?: string timestamp?: number | string } ``` ## Foundation types Foundation types describe the shape of devices, containers, alerts, site configuration, and settings data flowing through `@tetherto/mdk-react-devkit/foundation` components and API responses. They are organized into barrels including `alerts`, `config`, `device`, and `settings.types`. ### Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/foundation#prerequisites) ### Import @tetherto/mdk-react-devkit/foundation ```tsx Alert, Device, DeviceLast, DeviceInfo, ContainerSnap, ContainerStats, MinerStats, MinerConfig, SettingsUser, PermLevel, GlobalConfig, TimelineChartDataPoint, TimelineChartDataset, TimelineChartData, ChartRange, AxisTitleText, MetricsEfficiencyLogEntry, } from '@tetherto/mdk-react-devkit/foundation' ``` ### Alert types #### `Alert` @tetherto/mdk-react-devkit/foundation Raw alert record as it appears on a device's `alerts` array. ```tsx type Alert = { id?: string severity: string createdAt: number | string name: string description: string message?: string uuid?: string code?: string | number [key: string]: unknown } ``` The `severity` field uses string values like `critical`, `high`, `medium`, `low`. The open `[key: string]: unknown` index signature allows vendor-specific fields without breaking the type contract. #### `LogFormattedAlertData` @tetherto/mdk-react-devkit/foundation Alert reshaped for log display components (e.g., `AlertsLog`). ```tsx type LogFormattedAlertData = { title: string subtitle: string status: string severityLevel: number creationDate: number | string body: string id: string uuid?: string [key: string]: unknown } ``` ### Device types The `Device` family models everything that appears on the device explorer: miners, containers, power meters, temperature sensors, and cabinets. The shape is intentionally permissive (open index signatures, optional fields) because devices come from a live API that adds vendor-specific fields over time. #### `Device` @tetherto/mdk-react-devkit/foundation The root device record. Used pervasively in the [operations centre](/reference/ui/react/foundation/operations) components. ```tsx 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[] transformerTempSensor?: Device rootTempSensor?: Device [key: string]: unknown } ``` The `type` string discriminates devices by category (such as miner or container) and by vendor. The `last` field carries the latest snapshot from the device. #### `DeviceLast` @tetherto/mdk-react-devkit/foundation The latest reading wrapper that lives on `Device.last`. ```tsx type DeviceLast = { err?: string | null type?: string snap?: ContainerSnap alerts?: Alert[] | null [key: string]: unknown } ``` `err` is a connection or upstream error string when the device is unreachable. `snap` carries the actual stats and config payload. #### `DeviceInfo` @tetherto/mdk-react-devkit/foundation Identification and placement metadata that lives on `Device.info`. ```tsx type DeviceInfo = { container?: string pos?: string poolConfig?: string serialNum?: string macAddress?: string | null posHistory?: Partial [key: string]: unknown } ``` #### `PosHistoryEntry` @tetherto/mdk-react-devkit/foundation A single past placement of a miner. ```tsx type PosHistoryEntry = { container: string pos: string removedAt: number } ``` #### `DeviceData` @tetherto/mdk-react-devkit/foundation A flattened version of `Device` with a guaranteed (non-optional) `snap` field, returned by the `getDeviceData` helper. ```tsx type DeviceData = { id: string type: string tags?: string[] rack?: string snap: ContainerSnap alerts?: Alert[] username?: string info?: DeviceInfo containerId?: string address?: string err?: string [key: string]: unknown } ``` ### Container types #### `Container` @tetherto/mdk-react-devkit/foundation A `Device` specialized for containers, with container-specific `info` and `last` shapes. ```tsx type Container = { info?: Partial last?: Partial } & Device ``` #### `ContainerInfo` @tetherto/mdk-react-devkit/foundation Cooling, supply, and pressure metadata for a container. ```tsx type ContainerInfo = { container: string cooling_system: Record cdu: Record primary_supply_temp: number second_supply_temp1: number second_supply_temp2: number supply_liquid_temp: number supply_liquid_set_temp: number supply_liquid_pressure: number return_liquid_pressure: number } ``` #### `ContainerPosInfo` @tetherto/mdk-react-devkit/foundation Position descriptor for a device inside a container (PDU/socket coordinates). ```tsx type ContainerPosInfo = { containerInfo: Partial<{ container: string; type: string }> pdu: string | number socket: string | number pos: string [key: string]: unknown } ``` #### `ContainerLast` @tetherto/mdk-react-devkit/foundation Last-snapshot wrapper for a container. ```tsx type ContainerLast = { snap: { stats?: Partial } alerts: unknown[] | null err: string | null } ``` #### `ContainerSnap` @tetherto/mdk-react-devkit/foundation The `snap` payload found on `DeviceLast.snap` for both miners and containers. `stats` is what most components read. ```tsx type ContainerSnap = { stats?: Partial config?: Record } ``` #### `ContainerStats` @tetherto/mdk-react-devkit/foundation The big stats blob produced by every container snapshot. ```tsx 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_c: Partial frequency_mhz: Partial miner_specific: Partial [key: string]: unknown } ``` `status` is one of `running`, `offline`, `stopped` (see `CONTAINER_STATUS` in [Constants](/reference/app-toolkit/ui-devkit/constants#foundation-constants)). #### `ContainerSpecific` @tetherto/mdk-react-devkit/foundation Container-specific stats (currently the PDU array). ```tsx type ContainerSpecific = { pdu_data: Partial[] [key: string]: unknown } ``` #### `ContainerPduData` @tetherto/mdk-react-devkit/foundation Per-PDU power and status reading. ```tsx type ContainerPduData = { power_w: number status: number } ``` #### `StatsTemperatureC` @tetherto/mdk-react-devkit/foundation Temperature stats with per-chip detail. ```tsx type StatsTemperatureC = { avg: number min: number max: number chips: TempChipData[] [key: string]: unknown } ``` #### `StatsFrequencyMhz` @tetherto/mdk-react-devkit/foundation Frequency stats with per-chip detail. ```tsx type StatsFrequencyMhz = { avg: number chips: ChipData[] [key: string]: unknown } ``` #### `ChipData` / `TempChipData` @tetherto/mdk-react-devkit/foundation Per-chip readings. ```tsx type ChipData = { index: number current: number } type TempChipData = { index: number max?: number min?: number avg?: number } ``` #### `MinerSpecificStats` @tetherto/mdk-react-devkit/foundation Miner-specific stats blob. ```tsx type MinerSpecificStats = { upfreq_speed: number [key: string]: unknown } ``` ### Miner types #### `MinerStats` @tetherto/mdk-react-devkit/foundation Per-miner stats reported on each snapshot. ```tsx type MinerStats = { status?: string uptime_ms?: number power_w?: number hashrate_mhs?: MinerHashrateMhs poolHashrate?: string temperature_c?: { max?: number } } ``` #### `MinerHashrateMhs` @tetherto/mdk-react-devkit/foundation Hashrate readings, currently just the rolling 5-minute window. ```tsx type MinerHashrateMhs = { t_5m?: number } ``` #### `MinerInfo` @tetherto/mdk-react-devkit/foundation Identifying info for a miner (used by miner record cards). ```tsx type MinerInfo = { container?: string pos?: string macAddress?: string serialNum?: string } ``` #### `MinerConfig` @tetherto/mdk-react-devkit/foundation Mutable miner configuration. ```tsx type MinerConfig = { firmware_ver?: string power_mode?: string led_status?: boolean } ``` `power_mode` values come from `MINER_POWER_MODE` (`sleep`, `low`, `normal`, `high`). #### `MinerDeviceSnapshot` @tetherto/mdk-react-devkit/foundation Lightweight snapshot wrapper holding only `MinerConfig`. ```tsx type MinerDeviceSnapshot = { last?: { snap?: { config?: MinerConfig } } } ``` #### `MinerRecord` @tetherto/mdk-react-devkit/foundation Combined miner record used by list/table views. ```tsx 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 } ``` ### Power and cabinet types #### `PowerMeter` @tetherto/mdk-react-devkit/foundation Minimal power meter reading shape. ```tsx type PowerMeter = { last?: { snap?: { stats?: { power_w?: number } } } } ``` #### `LvCabinetRecord` @tetherto/mdk-react-devkit/foundation LV cabinet record carrying its associated power meters. ```tsx type LvCabinetRecord = { id: string powerMeters?: PowerMeter[] } ``` ### Config types #### `GlobalConfig` @tetherto/mdk-react-devkit/foundation Site-wide configuration from your API or store, including nominal targets for reporting dashboards. Load this shape in your app from your API or store and pass it into foundation reporting UI as needed. ```tsx type GlobalConfig = { nominalSiteHashrate_MHS?: number nominalAvailablePowerMWh?: number nominalPowerConsumption_MW?: number nominalWeightedAvgEfficiency_WThs?: number nominalMinerCapacity?: number isAutoSleepAllowed?: boolean siteEnergyDataThresholdMWh?: number [key: string]: unknown } ``` | Field | Type | Description | |-------|------|-------------| | `nominalSiteHashrate_MHS` | `number` | Nominal site hashrate (MH/s) | | `nominalAvailablePowerMWh` | `number` | Nominal available power (MWh on the wire) | | `nominalPowerConsumption_MW` | `number` | Nominal power consumption (MW) | | `nominalWeightedAvgEfficiency_WThs` | `number` | Nominal weighted average efficiency (W/TH) | | `nominalMinerCapacity` | `number` | Nominal miner capacity | | `isAutoSleepAllowed` | `boolean` | Whether auto-sleep is permitted for the site | | `siteEnergyDataThresholdMWh` | `number` | Energy data threshold (MWh) | | `[key: string]` | `unknown` | Additional API fields without breaking the type | ### Timeline chart types Types for [`TimelineChart`](/reference/ui/react/foundation/dashboard/charts#timelinechart) in `@tetherto/mdk-react-devkit/foundation`. Each segment uses `x: [startMs, endMs]` and `y` matching a row in `labels`. #### `TimelineChartDataPoint` @tetherto/mdk-react-devkit/foundation One horizontal segment on a timeline row. ```tsx type TimelineChartDataPoint = { x: [number, number] y: string | undefined } ``` | Field | Type | Description | |-------|------|-------------| | `x` | `[number, number]` | Segment start and end (epoch ms) | | `y` | `string \| undefined` | Row label; must match an entry in `TimelineChartData.labels` | #### `TimelineChartDataset` @tetherto/mdk-react-devkit/foundation A named group of segments (legend entry). ```tsx type TimelineChartDataset = { label: string data: TimelineChartDataPoint[] borderColor?: string[] backgroundColor?: string[] color?: string } ``` #### `TimelineChartData` @tetherto/mdk-react-devkit/foundation Full payload for `initialData` and `newData`. ```tsx type TimelineChartData = { labels: string[] datasets: TimelineChartDataset[] } ``` | Field | Type | Description | |-------|------|-------------| | `labels` | `string[]` | Row names (Y axis) | | `datasets` | `TimelineChartDataset[]` | Segment groups keyed by `label` | #### `ChartRange` @tetherto/mdk-react-devkit/foundation Visible time window for the `range` prop. ```tsx type ChartRange = { min: Date | number max: Date | number } ``` #### `AxisTitleText` @tetherto/mdk-react-devkit/foundation Axis title strings for `axisTitleText`. ```tsx type AxisTitleText = { x: string y: string } ``` ### Metrics efficiency types Types for **`/auth/metrics/efficiency`**, used by the site view tab on [`OperationsEfficiency`](/reference/ui/react/foundation/reporting/operations-efficiency). #### `MetricsEfficiencyLogEntry` @tetherto/mdk-react-devkit/foundation One point on the site efficiency time series. ```tsx type MetricsEfficiencyLogEntry = { ts: number efficiencyWThs: number } ``` | Field | Type | Description | |-------|------|-------------| | `ts` | `number` | Timestamp (epoch ms) | | `efficiencyWThs` | `number` | Site efficiency (W/TH) at `ts` | #### `MetricsEfficiencySummary` @tetherto/mdk-react-devkit/foundation Summary block returned with the efficiency metrics response. ```tsx type MetricsEfficiencySummary = { avgEfficiencyWThs: number | null } ``` #### `MetricsEfficiencyResponse` @tetherto/mdk-react-devkit/foundation Wrapper for the efficiency metrics endpoint (`log` entries plus summary), using the shared `MetricsResponse` shape from `@tetherto/mdk-react-devkit/foundation`. ### Settings types #### `SettingsUser` @tetherto/mdk-react-devkit/foundation A user record as it appears in user-management lists. ```tsx type SettingsUser = { id: string name?: string email: string role: string last_login?: string lastActive?: string [key: string]: unknown } ``` #### `RoleOption` @tetherto/mdk-react-devkit/foundation Role option for select dropdowns. Also exported as the array `USER_ROLES` in [Constants](/reference/app-toolkit/ui-devkit/constants#foundation-constants). ```tsx type RoleOption = { label: string value: string } ``` #### `PermLevel` @tetherto/mdk-react-devkit/foundation Permission level values. ```tsx type PermLevel = 'rw' | 'r' | false ``` #### `RolesPermissionsData` @tetherto/mdk-react-devkit/foundation The shape consumed by [`RBACControlSettings`](/reference/ui/react/foundation/settings/access-control). ```tsx type RolesPermissionsData = { permissions: Record> labels: Record } ``` #### `SettingsExportData` @tetherto/mdk-react-devkit/foundation The export envelope produced and consumed by [`ImportExportSettings`](/reference/ui/react/foundation/settings/import-export). Generic `TExtra` lets you attach app-specific extras. ```tsx type SettingsExportData = Record> = { headerControls?: Record featureFlags?: Record timestamp?: string version?: string } & TExtra ``` #### `ImportResult` @tetherto/mdk-react-devkit/foundation Result returned from settings import operations. ```tsx type ImportResult = { success: boolean applied?: string[] errors?: string[] message?: string } ``` # Utilities (/reference/app-toolkit/ui-devkit/utilities) This page documents helper functions exported by the MDK packages. - [Core utilities](#core-utilities) ships **15 utility modules** with functions for formatting, dates, validation, conversions, class-name merging, and more - [Foundation utilities](#foundation-utilities) currently ships a single public utility module (`settings-utils`) for parsing, validating, and exporting settings JSON - [UI Foundation alert utilities](#ui-foundation-alert-utilities) ships query-parameter builders and time-range chunking helpers for the Alerts feature ## Core utilities Helper functions exported by `@tetherto/mdk-react-devkit/core` for formatting, dates, validation, conversions, and class-name merging. ### Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/core#prerequisites) ### Import @tetherto/mdk-react-devkit/core ```tsx formatNumber, formatHashrate, formatDate, formatRelativeTime, cn, isEmpty, isValidEmail, } from '@tetherto/mdk-react-devkit/core' ``` ### Formatting utilities #### `formatNumber` @tetherto/mdk-react-devkit/core Format numbers with locale formatting and configurable options. ```tsx formatNumber(1234.567) // "1,234.57" formatNumber(null) // "-" formatNumber(1234, { minimumFractionDigits: 2 }) // "1,234.00" formatNumber(undefined, {}, 'N/A') // "N/A" ``` #### `formatHashrate` @tetherto/mdk-react-devkit/core Format hashrate values with rounding. ```tsx formatHashrate(150.456) // "150.46" formatHashrate(null) // "-" ``` #### `formatCurrency` @tetherto/mdk-react-devkit/core Format currency values. ```tsx formatCurrency(1234.56, 'USD') // "$1,234.56" formatCurrency(0.00012345, 'BTC') // "₿0.00012345" ``` #### `getPercentFormattedNumber` @tetherto/mdk-react-devkit/core Format numbers as percentages. ```tsx getPercentFormattedNumber(0.75) // "75%" getPercentFormattedNumber(0.1234, 1) // "12.3%" ``` #### `formatValueUnit` @tetherto/mdk-react-devkit/core Format value-unit objects. ```tsx formatValueUnit(150, 'TH/s') // "150 TH/s" ``` ### Date utilities #### `formatDate` @tetherto/mdk-react-devkit/core Format dates with customizable patterns. ```tsx formatDate(new Date()) // "Jan 15, 2025" formatDate(1705334400000, { format: 'yyyy-MM-dd' }) // "2025-01-15" ``` #### `formatRelativeTime` @tetherto/mdk-react-devkit/core Format dates as relative time strings. ```tsx formatRelativeTime(new Date(Date.now() - 3600000)) // "1h ago" formatRelativeTime(new Date(Date.now() - 86400000)) // "1d ago" ``` #### `formatChartDate` @tetherto/mdk-react-devkit/core Format timestamps for chart display. ```tsx formatChartDate(1705334400) // "Jan 15" formatChartDate(1705334400, true) // "Jan 15, 2025" ``` #### `isValidTimestamp` @tetherto/mdk-react-devkit/core Check if a timestamp is valid. ```tsx isValidTimestamp(1705334400000) // true isValidTimestamp('invalid') // false ``` #### `parseMonthLabelToDate` @tetherto/mdk-react-devkit/core Parse month labels to `Date` objects. ```tsx parseMonthLabelToDate('01-26') // Date(2026, 0, 1) parseMonthLabelToDate('03-2025') // Date(2025, 2, 1) ``` #### `getPastDateFromDate` @tetherto/mdk-react-devkit/core Get a date in the past. ```tsx getPastDateFromDate({ dateTs: Date.now(), days: 7 }) // 7 days ago ``` ### Validation utilities #### `isEmpty` @tetherto/mdk-react-devkit/core Check if a value is empty. ```tsx isEmpty(null) // true isEmpty('') // true isEmpty([]) // true isEmpty({}) // true isEmpty('hello') // false isEmpty([1, 2, 3]) // false ``` #### `isValidEmail` @tetherto/mdk-react-devkit/core Validate email addresses. ```tsx isValidEmail('user@example.com') // true isValidEmail('invalid') // false ``` #### `isValidUrl` @tetherto/mdk-react-devkit/core Validate URLs. ```tsx isValidUrl('https://example.com') // true isValidUrl('not-a-url') // false ``` #### `isNil` @tetherto/mdk-react-devkit/core Check if value is `null` or `undefined`. ```tsx isNil(null) // true isNil(undefined) // true isNil(0) // false isNil('') // false ``` #### `isPlainObject` @tetherto/mdk-react-devkit/core Check if value is a plain object. ```tsx isPlainObject({}) // true isPlainObject({ a: 1 }) // true isPlainObject([]) // false isPlainObject(new Date()) // false ``` ### Class name utilities #### `cn` @tetherto/mdk-react-devkit/core Merge class names using [clsx](https://github.com/lukeed/clsx) and [tailwind-merge](https://github.com/dcastil/tailwind-merge). ```tsx cn('px-4', 'py-2') // "px-4 py-2" cn('text-red', isError && 'bg-red') // conditional classes cn('p-4', { 'hidden': !visible }) // object syntax ``` ### Conversion utilities #### `toMW` / `toMWh` @tetherto/mdk-react-devkit/core Convert watts to megawatts. ```tsx toMW(1000000) // 1 toMWh(1000000) // 1 ``` #### `toPHS` @tetherto/mdk-react-devkit/core Convert raw hashrate to `PH/s`. ```tsx toPHS(1000000000000000) // 1 ``` #### `convertMpaToBar` @tetherto/mdk-react-devkit/core Convert pressure units. ```tsx convertMpaToBar(0.1) // 1 ``` #### `unitToKilo` @tetherto/mdk-react-devkit/core Convert to kilo units. ```tsx unitToKilo(1000) // 1 ``` ### Number utilities #### `percentage` @tetherto/mdk-react-devkit/core Calculate percentage. ```tsx percentage(25, 100) // 25 percentage(1, 4) // 25 ``` #### `getPercentChange` @tetherto/mdk-react-devkit/core Calculate percentage change. ```tsx getPercentChange(110, 100) // 10 getPercentChange(90, 100) // -10 ``` #### `convertUnits` @tetherto/mdk-react-devkit/core Convert between SI-prefix units. ```tsx convertUnits(1, 'k', 'M') // 0.001 convertUnits(1000, 'decimal', 'k') // 1 ``` #### `safeNumber` @tetherto/mdk-react-devkit/core Safely convert to number. ```tsx safeNumber('123') // 123 safeNumber('invalid') // 0 safeNumber(null) // 0 ``` ### String utilities #### `toTitleCase` @tetherto/mdk-react-devkit/core Convert string to Title Case. ```tsx toTitleCase('hello world') // "Hello World" ``` #### `formatMacAddress` @tetherto/mdk-react-devkit/core Format MAC addresses. ```tsx formatMacAddress('aa:bb:cc:dd:ee:ff') // "AA:BB:CC:DD:EE:FF" ``` #### `safeString` @tetherto/mdk-react-devkit/core Safely convert to string. ```tsx safeString(123) // "123" safeString(null) // "" ``` ### Time utilities #### `secondsToMs` @tetherto/mdk-react-devkit/core Convert seconds to milliseconds. ```tsx secondsToMs(60) // 60000 ``` #### `breakTimeIntoIntervals` @tetherto/mdk-react-devkit/core Split `[start, end]` into consecutive windows of `intervalMs`. Re-exported from `@tetherto/mdk-ui-foundation`. ```tsx breakTimeIntoIntervals(start, end, 3_600_000) // Array of 1-hour { start, end } windows ``` #### `timeRangeWalker` @tetherto/mdk-react-devkit/core Generator for iterating through time ranges. ```tsx for (const interval of timeRangeWalker(start, end, duration)) { // Process each interval } ``` ### Color utilities #### `hexToRgba` @tetherto/mdk-react-devkit/core Convert hex color to rgba. ```tsx hexToRgba('#72F59E', 0.5) // "rgba(114, 245, 158, 0.5)" ``` ### Array utilities #### `getNestedValue` @tetherto/mdk-react-devkit/core Get nested value by dot-path. ```tsx getNestedValue({ a: { b: 1 } }, 'a.b') // 1 ``` #### `getWeightedAverage` @tetherto/mdk-react-devkit/core Calculate weighted average. ```tsx getWeightedAverage(items, 'value', 'weight') ``` #### `circularArrayAccess` @tetherto/mdk-react-devkit/core Create infinite cycling generator. ```tsx const colors = circularArrayAccess(['red', 'green', 'blue']) colors.next().value // 'red' colors.next().value // 'green' colors.next().value // 'blue' colors.next().value // 'red' (cycles) ``` ## Foundation utilities Helpers exported by `@tetherto/mdk-react-devkit/foundation` for filtering, formatting, validating, parsing, and exporting settings data. ### Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/foundation#prerequisites) ### Import @tetherto/mdk-react-devkit/foundation ```tsx filterUsers, formatRoleLabel, formatLastActive, validateSettingsJson, parseSettingsFile, exportSettingsToFile, } from '@tetherto/mdk-react-devkit/foundation' ``` ### Settings utilities #### `filterUsers` @tetherto/mdk-react-devkit/foundation Filter a `SettingsUser[]` list by email substring (case-insensitive) and exact role match. Used by the user-management table search. ```tsx filterUsers({ users, email: 'alice', // partial, case-insensitive match on user.email role: 'admin', // exact match on user.role; pass null to skip }) ``` | Parameter | Type | Description | |-----------|------|-------------| | `users` | `SettingsUser[]` | Source list | | `email` | `string \| null \| undefined` | Substring filter on `email` (case-insensitive). Skipped when falsy. | | `role` | `string \| null \| undefined` | Exact role match. Skipped when falsy. | #### `formatRoleLabel` @tetherto/mdk-react-devkit/foundation Convert a snake case role identifier to a human-readable Title Case label. ```tsx formatRoleLabel('site_manager') // "Site Manager" formatRoleLabel('reporting_tool_manager') // "Reporting Tool Manager" formatRoleLabel('admin') // "Admin" ``` #### `formatLastActive` @tetherto/mdk-react-devkit/foundation Format a timestamp string as `MM/DD/YYYY - HH:MM`. Returns `'-'` when the input is missing or invalid. ```tsx formatLastActive('2025-01-15T14:30:00Z') // "01/15/2025 - 14:30" formatLastActive(undefined) // "-" formatLastActive('not-a-date') // "-" ``` #### `validateSettingsJson` @tetherto/mdk-react-devkit/foundation Type-guard that checks whether an unknown value is a valid `SettingsExportData`. Returns `true` if the value is an object that contains at least one of `headerControls`, `featureFlags`, or `timestamp`. ```tsx if (validateSettingsJson(parsed)) { // parsed is now narrowed to SettingsExportData } ``` #### `parseSettingsFile` @tetherto/mdk-react-devkit/foundation Read a `File` containing settings JSON, validate it, and resolve to `SettingsExportData`. Rejects on invalid JSON, an invalid format, or a file read error. ```tsx try { const settings = await parseSettingsFile(file) applySettings(settings) } catch (err) { notifyError('Could not import settings', err.message) } ``` | Throws | Reason | |--------|--------| | `` `Invalid settings file format. Please ensure the file is a valid ${WEBAPP_NAME} settings export.` `` | The JSON parsed but didn't match the `SettingsExportData` shape. The literal interpolates [`WEBAPP_NAME`](/reference/app-toolkit/ui-devkit/constants#webapp_name). | | `Failed to parse JSON file. Please ensure the file is valid JSON.` | The file contents weren't valid JSON. | | `Failed to read file.` | The browser couldn't read the file. | #### `exportSettingsToFile` @tetherto/mdk-react-devkit/foundation Serialize `SettingsExportData` to JSON, package it as a downloadable Blob, and trigger a browser download. Returns the generated filename (e.g., `mdk-settings-2025-01-15T14-30-00-000Z.json`). The filename prefix is fixed in the foundation kit and does not interpolate [`WEBAPP_NAME`](/reference/app-toolkit/ui-devkit/constants#webapp_name). ```tsx const filename = exportSettingsToFile({ headerControls: { poolMiners: true, consumption: false }, featureFlags: { betaCharts: true }, timestamp: new Date().toISOString(), version: '1.0.0', }) ``` ## UI Foundation alert utilities `@tetherto/mdk-ui-foundation` ships two utility modules for the Alerts feature: query-parameter builders (`alert-queries`) and time-range chunking helpers (`historical-log-chunks`). These are the source consumed directly by the `useCurrentAlertDevices` and `useHistoricalAlerts` adapter hooks. ### Prerequisites - Complete the [@tetherto/mdk-ui-foundation installation](/reference/app-toolkit/ui-foundation) ### Import @tetherto/mdk-ui-foundation ```tsx ONE_DAY_MS, DEFAULT_HISTORICAL_WINDOW_MS, getDefaultHistoricalAlertsRange, buildCurrentAlertDevicesParams, buildHistoricalAlertsParams, breakTimeIntoIntervals, mergeAlertsByUuid, fetchHistoricalAlertsInChunks, } from '@tetherto/mdk-ui-foundation' ``` ### Alert query builders #### `ONE_DAY_MS` @tetherto/mdk-ui-foundation One day in milliseconds (`86_400_000`). The fetch-window size used by the historical-alerts data path. ```tsx ONE_DAY_MS // 86400000 ``` #### `DEFAULT_HISTORICAL_WINDOW_MS` @tetherto/mdk-ui-foundation Default historical-alerts look-back window — 14 days (`14 * ONE_DAY_MS`). Matches the devkit [``](/reference/ui/react/foundation/alerts) feature default; wider ranges fan out into more 24-hour requests. ```tsx DEFAULT_HISTORICAL_WINDOW_MS // 1209600000 (14 days in ms) ``` #### `getDefaultHistoricalAlertsRange` @tetherto/mdk-ui-foundation Returns the default historical-alerts range: the last `DEFAULT_HISTORICAL_WINDOW_MS` ending now. Used by the devkit [``](/reference/ui/react/foundation/alerts) feature and the shell Alerts page to seed their range state. Pass `now` to fix the upper bound in tests. ```tsx const range = getDefaultHistoricalAlertsRange() // { start: <14 days ago>, end: } // Injectable for tests getDefaultHistoricalAlertsRange(1_700_000_000_000) // { start: 1_700_000_000_000 - DEFAULT_HISTORICAL_WINDOW_MS, end: 1_700_000_000_000 } ``` | Parameter | Status | Type | Default | Description | |-----------|--------|------|---------|-------------| | `now` | Optional | `number` | `Date.now()` | Upper bound of the window (ms epoch). | #### `buildCurrentAlertDevicesParams` @tetherto/mdk-ui-foundation Builds the `list-things` query params for the current-alerts table: every device that currently carries one or more alerts, with a 1 000 device limit. Consumed by `useCurrentAlertDevices`. `filterTags` widen the server-side selector so the fetch narrows with the active search chips (`ip-`, `sn-`, `mac-`, `firmware-`) instead of relying on client-side filtering alone. ```tsx // All devices with active alerts const params = buildCurrentAlertDevicesParams() // Narrowed to devices matching the active search chips const filtered = buildCurrentAlertDevicesParams(['ip-192.168.1.1', 'sn-ABC123']) ``` | Parameter | Status | Type | Default | Description | |-----------|--------|------|---------|-------------| | `filterTags` | Optional | `string[]` | `[]` | Active alert search chips to narrow the selector server-side. | #### `buildHistoricalAlertsParams` @tetherto/mdk-ui-foundation Builds the `history-log` query params for a single alerts window (`logType: 'alerts'`). Called once per 24-hour sub-window by the chunked fetch. ```tsx const params = buildHistoricalAlertsParams({ start: range.start, end: range.end }) // { logType: 'alerts', start: ..., end: ... } ``` | Parameter | Status | Type | Default | Description | |-----------|--------|------|---------|-------------| | `range` | Required | `HistoricalAlertsRange` | none | `{ start: number; end: number }` — ms epoch bounds for the window. | ### Historical alert chunking #### `breakTimeIntoIntervals` @tetherto/mdk-ui-foundation Splits `[start, end]` into consecutive windows of `intervalMs` (default `ONE_DAY_MS`); the final window is clamped to `end`. Returns an empty array for an empty or inverted range. ```tsx // 24-hour windows (default) const windows = breakTimeIntoIntervals(start, end) // Custom window size const hourly = breakTimeIntoIntervals(start, end, 3_600_000) ``` | Parameter | Status | Type | Default | Description | |-----------|--------|------|---------|-------------| | `start` | Required | `number` | none | Range start (ms epoch). | | `end` | Required | `number` | none | Range end (ms epoch). | | `intervalMs` | Optional | `number` | `ONE_DAY_MS` | Window size in ms. | Returns `TimeInterval[]` where each element is `{ start: number; end: number }`. #### `mergeAlertsByUuid` @tetherto/mdk-ui-foundation Concatenates `next` onto `prev`, replacing any row that shares a `uuid` (later windows win) and appending the rest. Rows without a `uuid` are always appended. ```tsx const merged = mergeAlertsByUuid(previousAlerts, newAlerts) ``` | Parameter | Status | Type | Default | Description | |-----------|--------|------|---------|-------------| | `prev` | Required | `T[]` | none | Existing alert rows. | | `next` | Required | `T[]` | none | Rows from the latest window fetch. | Returns `T[]` where `T extends { uuid?: string }`. #### `fetchHistoricalAlertsInChunks` @tetherto/mdk-ui-foundation Fetches a historical-alerts range as successive 24-hour windows (oldest to newest), merging results by `uuid`. Individual window failures are swallowed so one bad request does not drop the whole range. The loop bails out early when `options.signal` is aborted. ```tsx fetchHistoricalAlertsInChunks, buildHistoricalAlertsParams, } from '@tetherto/mdk-ui-foundation' const alerts = await fetchHistoricalAlertsInChunks( { start: range.start, end: range.end }, async (window) => { const result = await myApi.historyLog(buildHistoricalAlertsParams(window)) return result.data }, { signal: abortController.signal }, ) ``` | Parameter | Status | Type | Default | Description | |-----------|--------|------|---------|-------------| | `range` | Required | `{ start: number; end: number }` | none | Full date range to paginate over (ms epoch). | | `fetchWindow` | Required | `function` | none | Called once per 24-hour window, oldest first. Receives a `TimeInterval` and returns `Promise`. | | `options.intervalMs` | Optional | `number` | `ONE_DAY_MS` | Window size in ms. | | `options.signal` | Optional | `AbortSignal` | none | Checked between windows — aborts the loop early on range changes. | # UI Foundation (/reference/app-toolkit/ui-foundation) @tetherto/mdk-ui-foundation If you are building a React app with the MDK kit, start with the [React adapter](/tutorials/ui/react) instead. `` and adapter hooks such as `useAuth` and `useDevices` wrap this package so most React code never imports `@tetherto/mdk-ui-foundation` directly. Use this reference when you need headless store access outside React (logging, websocket setup, test helpers) or when authoring a future framework adapter. `@tetherto/mdk-ui-foundation` is the framework-agnostic headless layer of the [MDK App Toolkit](/concepts/stack/app-toolkit). It ships Zustand vanilla stores and a TanStack Query Core `QueryClient` factory. There are **no React imports** in this package. ## Subpath exports | Subpath | Purpose | |---------|---------| | `.` | Top-level barrel | | `./store` | Zustand vanilla stores | | `./query` | `QueryClient` factory, query keys, and query/mutation factories | | `./types` | Shared type contracts | | `./stores.json` | Machine-readable store manifest (generated at build time) | ## Stores Each store is a Zustand vanilla singleton. In a React app, read and update state through the matching adapter hook instead of importing stores directly. | Store | Summary | Adapter hook | |-------|---------|--------------| | `authStore` | Session token and permission payload | [`useAuth`](/reference/app-toolkit/hooks/state#useauth) | | `devicesStore` | Fleet device list and current selection | [`useDevices`](/reference/app-toolkit/hooks/state#usedevices) | | `timezoneStore` | Active operator timezone | [`useTimezone`](/reference/app-toolkit/hooks/state#usetimezone) | | `notificationStore` | Unread notification counter (`count`, `increment`, `decrement`, `reset`) | [`useNotifications`](/reference/app-toolkit/hooks/state#usenotifications) | | `actionsStore` | Pending device and pool action submissions | [`useActions`](/reference/app-toolkit/hooks/state#useactions) | Import from `@tetherto/mdk-ui-foundation/store` (or the top-level barrel). ## QueryClient factory `createMdkQueryClient` builds a TanStack Query Core client with environment-aware Gateway base URL resolution: 1. Explicit override (typically from ``) 2. Build-time env: `VITE_MDK_API_URL` (Vite) or `MDK_API_URL` (Node) 3. Default: `http://localhost:3000` The `./query` subpath also exports query key helpers and factories (`authQuery`, `devicesQuery`, `deviceQuery`, `telemetryQuery`). See [TanStack Query](https://tanstack.com/query/latest) for general usage. ## Headless read outside React Utility code can subscribe to a store without React: ```ts const token = authStore.getState().token const unsubscribe = authStore.subscribe((state) => { console.log('token changed', state.token) }) // later: unsubscribe() ``` `` wires these singleton stores into React and creates the shared `QueryClient` for adapter hooks. ## What's not here yet Throttled telemetry subscriptions, stale detection, and history ring buffers are not yet present. They will be added alongside the consuming code that requires them. ## Next steps - [React get started](/tutorials/ui/react): three-package install and `` - [Wire a React app](/tutorials/ui/react/tutorial): full adapter wiring walkthrough - [MDK App Toolkit](/concepts/stack/app-toolkit): where UI Foundation fits in the frontend stack # 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-ork` 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-ork`'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 an 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-ork` 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-ork` 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-ork` asks for it, precisely as dictated by the telemetry schemas in `mdk-contract.json`. - *Internal scheduling*: to achieve this without blocking `@tetherto/mdk-ork`, 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-ork` 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-ork` 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-ork`, 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-ork`. A concrete request and response pair, end to end: ```json // request: Gateway asks @tetherto/mdk-ork 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-ork 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-ork` detects its peer connection automatically | | `identity.request` | request | `@tetherto/mdk-ork` to Worker | Requests the worker's identity and managed devices | | `capability.request` | request | `@tetherto/mdk-ork` to Worker | Asks the worker to declare its full capability schema | | `state.pull` | request | `@tetherto/mdk-ork` to Worker | Worker returns a snapshot of state-machine status (low cadence tick, e.g., 60s) | | `telemetry.pull` | request | `@tetherto/mdk-ork` to Worker | Worker returns device metrics plus history (medium cadence tick, e.g., 10s) | | `command.request` | request | `@tetherto/mdk-ork` to Worker | Resolves the worker by `deviceId` and dispatches the command for execution | | `health.ping` | request | `@tetherto/mdk-ork` 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 Devkit (/reference/ui) ## TL;Dr Today the published React path is complete; other frameworks are on the [roadmap](/support/resources/roadmap) and will reuse the same headless core. The MDK React UI Devkit is a [three-layer stack](#how-the-layers-fit-together): - [`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation): headless Zustand stores and TanStack Query client factory (framework-agnostic) - [`@tetherto/mdk-react-adapter`](/quickstart/wire-react#wrap-your-app-in-mdkprovider): `` and store hooks; binds the core into your UI runtime (React today) - [`@tetherto/mdk-react-devkit`](/tutorials/ui/react): [`/core`](/reference/ui/react/core) primitives and [`/foundation`](/reference/ui/react/foundation) mining-domain components ## Framework quickstarts } title={React} href="/tutorials/ui/react" description={ Get started with the React MDK UI Devkit } /> Vue} href="/support/resources/roadmap" description={ <> Reactive hooks for Vue via @tetherto/mdk-vue Learn about the release schedule → } /> Svelte} href="/support/resources/roadmap" description={ <> Reactive hooks for Svelte via @tetherto/mdk-svelte Learn about the release schedule → } /> Web Components} href="/support/resources/roadmap" description={ <> Framework-agnostic Web Components via @tetherto/wc Learn about the release schedule → } /> ## How the layers fit together The MDK React UI Devkit and subsequent frameworks will share a similar architecture: ```mermaid graph TB subgraph headless [Layer 1: Headless] uiCore([mdk-ui-foundation
Zustand stores · Query client factory
Framework-agnostic]) end subgraph adapter [Layer 2: Framework adapter] reactAdapter([mdk-react-adapter
MdkProvider · store hooks
React today]) end subgraph uiLib [Layer 3: Framework UI library] reactDevkit([mdk-react-devkit
/core + /foundation
Primitives and mining-domain UI]) end uiCore --> reactAdapter reactAdapter --> reactDevkit style headless fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style adapter fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style uiLib fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` ## Next steps - [Quickstart](/quickstart/wire-react): integrate the three packages into your React app - [Explore the demo](/tutorials/ui/explore-the-demo): run the demo app in your browser # @tetherto/mdk-react-devkit (/reference/ui/react/core) [The core entrypoint](/support/resources/repositories) (`@tetherto/mdk-react-devkit/core`) provides the foundational **React** UI components, utilities, and theme system for the MDK React UI Devkit. This is the `/core` subpath of the devkit package, not the framework-agnostic [`@tetherto/mdk-ui-foundation`](/reference/app-toolkit/ui-foundation) headless package. Mining-domain components, hooks, and settings live on the [`/foundation`](/reference/ui/react/foundation) entrypoint. Both subpaths are exported from `@tetherto/mdk-react-devkit`. ## Prerequisites - 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. ## What's included ### Components Production-ready React components organized by category: | Category | Description | |----------|-------------| | [Forms](/reference/ui/react/core/components/forms) | Input and form control components | | [Overlays](/reference/ui/react/core/components/overlays) | Dialogs, popovers, tooltips, and toasts | | [Data display](/reference/ui/react/core/components/data-display) | Cards, tables, tags, and data presentation | | [Charts](/reference/ui/react/core/components/charts) | Data visualization components | | [Navigation](/reference/ui/react/core/components/navigation) | Sidebar, tabs, and breadcrumbs | | [Loading](/reference/ui/react/core/components/loading) | Spinners, loaders, and progress indicators | | [Errors](/reference/ui/react/core/components/errors) | Error boundaries, error cards, and alerts | | [Logs](/reference/ui/react/core/components/logs) | Log display components | See the [Components reference](/reference/ui/react/core/components) for the full list with demo links. ### Icons 70+ purpose-built icons for Bitcoin mining applications: - Navigation icons (Dashboard, Farms, Inventory, etc.) - Status icons (Mining, Offline, Error, etc.) - Weather icons (Sunny, Cloudy, Rainy, etc.) - Alarm icons (Temperature, Pressure, Fluid, etc.) ### Utilities Helper functions for common operations: - [`formatNumber`](/reference/app-toolkit/ui-devkit/utilities#formatnumber), [`formatHashrate`](/reference/app-toolkit/ui-devkit/utilities#formathashrate), [`formatCurrency`](/reference/app-toolkit/ui-devkit/utilities#formatcurrency): Number formatting - [`formatDate`](/reference/app-toolkit/ui-devkit/utilities#formatdate), [`formatRelativeTime`](/reference/app-toolkit/ui-devkit/utilities#formatrelativetime): Date formatting - [`cn`](/reference/app-toolkit/ui-devkit/utilities#cn): Class name merging ([clsx](https://github.com/lukeed/clsx) wrapper) - Conversion utilities for energy, hashrate, and units - Validation utilities for email, URL, and empty checks ### Theme system CSS custom properties and utilities for consistent styling: - Color tokens (primary, gray scales) - Spacing scale - Border radius scale - Font size scale - Light/dark mode support ### Types TypeScript types for type-safe development: - [`ComponentSize`](/reference/app-toolkit/ui-devkit/types#componentsize), [`ButtonVariant`](/reference/app-toolkit/ui-devkit/types#buttonvariant), [`ColorVariant`](/reference/app-toolkit/ui-devkit/types#colorvariant) - [`Status`](/reference/app-toolkit/ui-devkit/types#status), [`PaginationParams`](/reference/app-toolkit/ui-devkit/types#paginationparams), [`ApiResponse`](/reference/app-toolkit/ui-devkit/types#apiresponse) - Chart types, table types, and utility types ## Reference Detailed reference material lives in the unified [Reference section](/reference). Core (`/core`) slices are: - [Constants](/reference/app-toolkit/ui-devkit/constants#core-constants): colors, units, currency, chart configs - [Types](/reference/app-toolkit/ui-devkit/types#core-types): UI primitives, API shapes, value-unit types - [Utilities](/reference/app-toolkit/ui-devkit/utilities#core-utilities): formatting, validation, conversions, `cn` and friends ## Import examples ```tsx // Import components // Import utilities // Import types // Import icons // Import styles (required for component styling) ``` # Components (/reference/ui/react/core/components) The `@tetherto/mdk-react-devkit/core` package provides production-ready React components. All components are built with accessibility in mind and support both light and dark themes. ## Import ### Prerequisites - Complete the [installation](/quickstart/wire-react) ```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 ``` - Import the core styles in your app's entry point: ```tsx ``` ### Import named components Declare the components you require: ```tsx ``` ## Supported components ## Styling Components use BEM-style CSS classes (e.g., `.mdk-button`, `.mdk-card__header`) for styling consistency and easy customization. ## Class name forwarding Every component forwards `className` to its root element, so you can apply layout or override styles without ejecting. This is assumed throughout and not repeated in every props table. Components with multiple styleable parts also expose **named** hooks (for example `contentClassName`, `indicatorClassName`, `dropdownClassName`) — those *are* listed in the relevant component's props table. ## Component design principles - **Composable**: Components are designed to work together - **Accessible**: Built with ARIA attributes and keyboard navigation - **Themeable**: Support light/dark modes via CSS custom properties - **Type-safe**: Full TypeScript support with exported prop types # Chart components (/reference/ui/react/core/components/charts) Chart components for visualizing time-series data, metrics, and statistics. Built on [Chart.js](https://www.chartjs.org/) and [Lightweight Charts](https://tradingview.github.io/lightweight-charts/), supporting: - Chart types: components that render datasets (`AreaChart`, `BarChart`, `DoughnutChart`, `GaugeChart`, `LineChart`) - Domain chart wrappers: opinionated chart components with preset layouts and series config (`AverageDowntimeChart`, `ThresholdLineChart`, `OperationsEnergyCostChart`) - [Composition elements](/reference/ui/react/core/components/charts/composition): layout chrome, legends, stats rows, and chart utilities (`ChartContainer`, `ChartStatsFooter`, `DetailLegend`, `MinMaxAvg`, helpers) ## Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/core#prerequisites) ## All chart type components ## `AreaChart` Presentational Chart.js area chart (line series with fill). Data and options follow the same Chart.js model as [`LineChart`](#linechart), without a separate MDK data wrapper type. ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `data` | Required | `ChartData` | none | [Chart.js](https://www.chartjs.org/) line chart `data` (datasets typically use `fill: true` for an area look) | | `options` | Optional | `ChartOptions` | none | [Chart.js](https://www.chartjs.org/) options (merged with defaults) | | `tooltip` | Optional | `ChartTooltipConfig` | none | Custom HTML tooltip config; when set, replaces the default Chart.js tooltip | | `height` | Optional | `number` | `300` | Chart height in pixels | | `className` | Optional | `string` | none | Root class name from the host app | ### Basic usage ```tsx const data = { labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'], datasets: [ { label: 'Revenue', data: [100, 120, 115, 134, 168], fill: true, backgroundColor: 'rgba(114, 245, 158, 0.2)', borderColor: '#72F59E', }, ], } ``` ## `BarChart` The `BarChart` component renders Chart.js bar or column series. It manages: 1. **Data** (`data`): labels and datasets passed to Chart.js. 2. **Chart options** (`options`): merged with MDK defaults. 3. **Formatting** (`formatYLabel`, `formatDataLabel`, `tooltip`): axis labels, data labels, and HTML tooltips. 4. **Presentation** (`isStacked`, `isHorizontal`, `showLegend`, `legendPosition`, `legendAlign`, `showDataLabels`): layout and legend. 5. **Size** (`height`): chart height in pixels. ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `data` | Required | `ChartData` | none | [Chart.js](https://www.chartjs.org/) data object | | `options` | Optional | `ChartOptions` | none | [Chart.js](https://www.chartjs.org/) options (merged with defaults) | | `formatYLabel` | Optional | `function` | none | Format Y-axis tick labels | | `formatDataLabel` | Optional | `function` | none | Format data label values | | `tooltip` | Optional | `ChartTooltipConfig` | none | Custom HTML tooltip config | | `isStacked` | Optional | `boolean` | `false` | Stack bars on top of each other | | `isHorizontal` | Optional | `boolean` | `false` | Render bars horizontally | | `showLegend` | Optional | `boolean` | `true` | Show [Chart.js](https://www.chartjs.org/) legend | | `legendPosition` | Optional | `'top' \| 'right' \| 'bottom' \| 'left'` | `'top'` | Legend position | | `legendAlign` | Optional | `'start' \| 'center' \| 'end'` | `'start'` | Legend alignment | | `showDataLabels` | Optional | `boolean` | `false` | Show values above bars | | `height` | Optional | `number` | `300` | Chart height in pixels | ### Basic usage ```tsx const data = { labels: ['Jan', 'Feb', 'Mar', 'Apr'], datasets: [ { label: 'Hashrate (TH/s)', data: [120, 150, 180, 200], backgroundColor: '#72F59E', }, ], } ``` ### Data from hooks Hand-built Chart.js `data` (above) is valid. When app hooks return `{ labels, series }` declarative input, convert with [`buildBarChartData`](/reference/ui/react/core/components/charts/composition#hook-shaped-bar-data) in [chart utilities](/reference/ui/react/core/components/charts/composition#chart-utilities), optionally merge per-series `datalabels`, then pass the result to `data`. ### Stacked bar chart ```tsx const data = { labels: ['Site A', 'Site B', 'Site C'], datasets: [ { label: 'Online', data: [100, 80, 120], backgroundColor: '#72F59E' }, { label: 'Offline', data: [10, 20, 5], backgroundColor: '#FF6B6B' }, ], } ``` ### Horizontal bar chart ```tsx `${value} TH/s`} /> ``` ### With data labels ```tsx `${value.toFixed(1)}%`} /> ``` ## `DoughnutChart` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `data` | Required | `DoughnutChartDataset[]` | none | Array of `{ label, value, color? }` slices (see [data shape](#data-shape-for-doughnut-charts)) | | `unit` | Optional | `string` | `''` | Unit suffix shown in the default tooltip | | `options` | Optional | `ChartOptions` | none | [Chart.js](https://www.chartjs.org/) doughnut options (merged with defaults) | | `cutout` | Optional | `string` | `'75%'` | Inner radius cutout (doughnut ring thickness) | | `borderWidth` | Optional | `number` | `4` | Border width between segments | | `height` | Optional | `number` | `260` | Chart height in pixels | | `legendPosition` | Optional | `string` | `'top'` | Where to place the custom legend relative to the chart | | `tooltip` | Optional | `ChartTooltipConfig` | none | Custom HTML tooltip; when set, replaces the default doughnut tooltip | | `formatValue` | Optional | `(value: number) => string` | none | Formats slice values in the built-in legend and default tooltip (default: raw number) | | `className` | Optional | `string` | none | Root class name from the host app | #### Data shape for doughnut charts Pass `data` as an array of `{ label, value, color? }`. The chart maps this into Chart.js internally (`labels`, single dataset, segment colors). Omit `color` to use the default palette rotation. ### Basic usage ```tsx const data = [ { label: 'Online', value: 85, color: '#72F59E' }, { label: 'Offline', value: 10, color: '#FF6B6B' }, { label: 'Maintenance', value: 5, color: '#FFD700' }, ] ``` ## `GaugeChart` Speedometer-style presentational arc gauge for displaying a single normalized value, providing: 1. **Percent** (`percent`): fill level from `0` to `1` (values outside that range are clamped). 2. **Arc appearance** (`colors`, `arcWidth`, `nrOfLevels`): segment colors, relative thickness, and segment count. 3. **Center label** (`hideText`): percentage text in the center of the gauge (hidden when `hideText` is `true`). 4. **Accessibility** (`id`): stable id wired into SVG labels (defaults to `mdk-gauge-chart`). 5. **Layout** (`height`, `maxWidth`, `className`): container height, max width, and host root class. The `GaugeChart` is SVG-based; strokes and labels may paint past the host element’s layout bounds. For a hard edge, wrap the gauge in an element with **`overflow: hidden`**, or add vertical spacing. [`ChartContainer`](/reference/ui/react/core/components/charts/composition#chartcontainer) does **not** set overflow clipping on the chart slot—it is for title, loading/empty, and footer chrome like other charts (see [Composition](/reference/ui/react/core/components/charts/composition)); it may add spacing that makes overlap less visible, but it is not a substitute for clipping when you need it. ### Import ```tsx ``` The gauge is driven by a **normalized** `percent` in the range **0–1** (for example `0.75` is 75%). It is not a `value` / `max` API, and it does not accept `label` or `unit` props (see [`ChartContainer`](/reference/ui/react/core/components/charts/composition#chartcontainer) for layout notes). ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `percent` | Required | `number` | none | Fill level from `0` to `1` (clamped) | | `colors` | Optional | `string[]` | green → soft green → red (theme) | Arc segment colors as HEX strings | | `arcWidth` | Optional | `number` | `0.2` | Arc thickness as a fraction of the gauge radius (`0`–`1`) | | `nrOfLevels` | Optional | `number` | `3` | Number of colored segments on the arc | | `hideText` | Optional | `boolean` | `false` | Hide the percentage text in the center | | `id` | Optional | `string` | `'mdk-gauge-chart'` | Stable id for SVG accessibility labels | | `height` | Optional | `number` \| `string` | `200` | Container height (pixels or CSS length, e.g. `'50%'`) | | `maxWidth` | Optional | `number` | `500` | Maximum width in pixels | | `className` | Optional | `string` | none | Root class name from the host app | ### Basic usage ```tsx ``` ### In a chart card (same shell as other charts) [`ChartContainer`](/reference/ui/react/core/components/charts/composition#chartcontainer) adds title, loading, and empty chrome; it does **not** apply `overflow: hidden` to the chart body. When you still see bleed next to siblings or footers, wrap `GaugeChart` in an extra element with **`overflow: hidden`**. ```tsx
``` ### Custom colors and arc ```tsx ``` ## `LineChart` The `LineChart` component renders time-series lines. It manages: 1. **Series data** (`data`): `LineChartData` with millisecond `x` values (see [data shape](#data-shape-for-line-charts)). 2. **Context** (`timeline`, `unit`): timeline identifier and unit label for tooltips. 3. **Formatting** (`yTicksFormatter`, `priceFormatter`, `customLabel`): tick and tooltip text. 4. **Presentation** (`backgroundColor`, `beginAtZero`, `showPointMarkers`, `showDateInTooltip`, `uniformDistribution`): axis and marker behavior. 5. **Size** (`height`): chart height in pixels. ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `data` | Required | `LineChartData` | none | Object with a `datasets` array (see [data shape](#data-shape-for-line-charts)) | | `timeline` | Optional | `string` | none | Timeline identifier | | `yTicksFormatter` | Optional | `function` | none | Format Y-axis ticks | | `priceFormatter` | Optional | `function` | none | Format tooltip values | | `customLabel` | Optional | `string` | none | Custom tooltip label | | `unit` | Optional | `string` | `''` | Unit label for values | | `backgroundColor` | Optional | `string` | none | Chart background color | | `beginAtZero` | Optional | `boolean` | `false` | Start Y-axis at zero | | `showPointMarkers` | Optional | `boolean` | `false` | Show data point markers | | `showDateInTooltip` | Optional | `boolean` | `false` | Show date in tooltip | | `uniformDistribution` | Optional | `boolean` | `false` | Uniform time distribution | | `height` | Optional | `number` | `240` | Chart height in pixels | #### Data shape for line charts `LineChartData` is `{ datasets: LineDataset[] }`. Each `LineDataset` has `label`, `borderColor`, optional `visible` / `borderWidth` / `extraTooltipData`, and `data: { x, y }[]` where `x` is a time value in **milliseconds** (for example from `Date.prototype.valueOf()`) and `y` is the series value (number, or `null` / `undefined` for gaps). ### Basic usage ```tsx const data = { datasets: [ { label: 'Hashrate', borderColor: '#72F59E', data: [ { x: 1704067200000, y: 150 }, { x: 1704153600000, y: 155 }, { x: 1704240000000, y: 160 }, ], }, ], } ``` ### Multiple lines ```tsx const hashrateData = [ { x: 1704067200000, y: 150 }, { x: 1704153600000, y: 155 }, ] const targetData = [ { x: 1704067200000, y: 140 }, { x: 1704153600000, y: 145 }, ] const data = { datasets: [ { label: 'Hashrate', borderColor: '#72F59E', data: hashrateData, }, { label: 'Target', borderColor: '#FFD700', data: targetData, }, ], } ``` ## `AverageDowntimeChart` Stacked bar chart showing monthly average downtime broken into **Curtailment** and **Op. Issues** series. Wraps [`ChartContainer`](/reference/ui/react/core/components/charts/composition#chartcontainer) and [`BarChart`](#barchart). It manages: 1. **Data** (`data`): period labels and per-series rate arrays (values are fractions 0–1). 2. **Formatting** (`yTicksFormatter`): formats Y-axis ticks, tooltips, and optional bar data labels. 3. **Presentation** (`showDataLabels`, `title`, `unit`): bar data labels and header text. 4. **Layout** (`height`, `barWidth`): chart height and bar width in pixels. 5. **State** (`isLoading`, `emptyMessage`): loading overlay and empty-state copy. ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `data` | Optional | `AverageDowntimeChartData` | none | Period labels and downtime rate arrays (see [data shape](#data-shape-for-averagedowntimechart)) | | `yTicksFormatter` | Optional | `function` | `defaultAverageDowntimeRateFormatter` | Formats Y-axis ticks, tooltips, and data labels; input values are 0–1 rates | | `showDataLabels` | Optional | `boolean` | `false` | Show formatted rate values above each bar segment | | `title` | Optional | `string` | `'Monthly Average Downtime'` | Header title | | `unit` | Optional | `string` | `'%'` | Unit label shown beneath the title | | `height` | Optional | `number` | `280` | Chart height in pixels | | `barWidth` | Optional | `number` | `38` | Bar width in pixels | | `isLoading` | Optional | `boolean` | `false` | When `true`, shows a loading overlay | | `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty state | | `className` | Optional | `string` | none | Root class name from the host app | #### Data shape for `AverageDowntimeChart` Pass `data` as an `AverageDowntimeChartData` object: | Field | Type | Description | |-------|------|-------------| | `labels` | `string[]` | Period labels (e.g. month names) | | `curtailment` | `number[]` | Curtailment downtime rates (0–1) per period | | `operationalIssues` | `number[]` | Operational issue downtime rates (0–1) per period | Both series arrays are optional; absent series are omitted from the chart. ### Basic usage ```tsx ``` ### Custom formatter `yTicksFormatter` receives raw 0–1 rate values and must return a display string. The default, `defaultAverageDowntimeRateFormatter`, multiplies by 100 and appends `%`: ```tsx AverageDowntimeChart, defaultAverageDowntimeRateFormatter, } from '@tetherto/mdk-react-devkit/core' // Override: show one decimal place `${(rate * 100).toFixed(1)}%`} showDataLabels /> ``` ### Exported helpers | Export | Role | |--------|------| | `buildAverageDowntimeBarChartData` | Converts `AverageDowntimeChartData` into Chart.js `data` | | `buildAverageDowntimeTooltip` | Builds the HTML tooltip config for the stacked bars | | `defaultAverageDowntimeRateFormatter` | Default Y-axis / label formatter (rate × 100, `%` suffix) | | `hasAverageDowntimeData` | Returns `true` when `data` has at least one non-empty series | ## `ThresholdLineChart` Time-series line chart with optional horizontal threshold bands. Wraps [`ChartContainer`](/reference/ui/react/core/components/charts/composition#chartcontainer) and [`LineChart`](#linechart). It manages: 1. **Data** (`data`): one or more line series plus optional threshold lines. 2. **Formatting** (`yTicksFormatter`): formats Y-axis ticks. 3. **Presentation** (`title`, `unit`, `isTall`, `isLegendVisible`): header, height variant, and legend toggle. 4. **Layout** (`height`): explicit chart height in pixels (overrides `isTall`). 5. **State** (`emptyMessage`): empty-state copy. ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `data` | Optional | `ThresholdLineChartData` | none | Series and optional thresholds (see [data shape](#data-shape-for-thresholdlinechart)) | | `yTicksFormatter` | Optional | `function` | unit-aware default | Formats Y-axis tick labels; defaults to `"${value} ${unit}"` when `unit` is set, otherwise `String(value)` | | `title` | Optional | `string` | none | Header title; rendered as `"${title} (${unit})"` when both are set | | `unit` | Optional | `string` | none | Unit appended to the title and used by the default tick formatter | | `isTall` | Optional | `boolean` | `false` | When `true`, uses a taller default height of `360px` instead of `280px` | | `isLegendVisible` | Optional | `boolean` | `true` | Show the interactive legend with dataset visibility toggles | | `height` | Optional | `number` | `280` (`360` when `isTall`) | Chart height in pixels; explicit value overrides `isTall` | | `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty state | | `className` | Optional | `string` | none | Root class name from the host app | #### Data shape for `ThresholdLineChart` `ThresholdLineChartData` holds series and optional threshold lines: | Field | Type | Description | |-------|------|-------------| | `series` | `ThresholdLineChartSeries[]` | Line series to render | | `thresholds` | `ThresholdLineChartThreshold[]` | Optional horizontal reference lines | Each `ThresholdLineChartSeries`: | Field | Type | Description | |-------|------|-------------| | `label` | `string` | Series label shown in the legend | | `points` | `ThresholdLineChartPoint[]` | Data points (`{ timestamp: string \| number, value: number }`) | | `color` | `string` | Optional line color | | `fill` | `boolean` | Optional area fill below the line | Each `ThresholdLineChartThreshold`: | Field | Type | Description | |-------|------|-------------| | `label` | `string` | Label shown in the legend | | `value` | `number` | Y-axis value at which the horizontal line is drawn | | `color` | `string` | Optional line color | ### Basic usage ```tsx ``` ### Tall variant ```tsx ``` ### Exported helpers | Export | Role | |--------|------| | `toThresholdLineChartData` | Converts `ThresholdLineChartData` into `LineChartData` for the underlying `LineChart` | | `hasNonZeroData` | Returns `true` when `data` contains at least one series with a non-zero value | ## `OperationsEnergyCostChart` Doughnut chart comparing operational costs against energy costs in USD per MWh. Wraps [`ChartContainer`](/reference/ui/react/core/components/charts/composition#chartcontainer) and [`DoughnutChart`](#doughnutchart). It manages: 1. **Data** (`data`): operational and energy cost values in USD. 2. **Presentation** (`title`, `unit`): header text and unit label. 3. **Layout** (`height`): chart height in pixels. 4. **State** (`isLoading`, `emptyMessage`): loading overlay and empty-state copy. ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `data` | Optional | `OperationsEnergyCostChartData` | none | Cost values in USD (see [data shape](#data-shape-for-operationsenergycostchart)) | | `title` | Optional | `string` | `'Operations vs Energy Cost'` | Header title | | `unit` | Optional | `string` | `'$/MWh'` | Unit label shown beneath the title | | `height` | Optional | `number` | `200` | Chart height in pixels | | `isLoading` | Optional | `boolean` | `false` | When `true`, shows a loading overlay | | `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty state | | `className` | Optional | `string` | none | Root class name from the host app | #### Data shape for `OperationsEnergyCostChart` `OperationsEnergyCostChartData` has two optional numeric fields: | Field | Type | Description | |-------|------|-------------| | `operationalCostsUSD` | `number` | Operational costs in USD | | `energyCostsUSD` | `number` | Energy costs in USD | Both fields are optional. Slices with a zero or absent value are omitted from the doughnut. ### Basic usage ```tsx ``` ### Exported helpers | Export | Role | |--------|------| | `buildOperationsEnergyCostSlices` | Converts `OperationsEnergyCostChartData` into `DoughnutChartDataset[]` slices, omitting zero-value entries | | `buildOperationsEnergyCostTooltip` | Builds the HTML tooltip config with two-decimal formatting | | `hasOperationsEnergyCostData` | Returns `true` when at least one cost field is a positive number | # Chart composition (/reference/ui/react/core/components/charts/composition) Compose chart types from [Chart components](/reference/ui/react/core/components) with wrappers and helpers. `ChartContainer` adds title, loading, and empty states; `ChartStatsFooter` and `DetailLegend` add summary rows and rich legends (for example [`LineChartCard`](/reference/ui/react/foundation/dashboard/charts#linechartcard)). ## Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/core#prerequisites) ## Components ## `ChartContainer` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `children` | Required | `ReactNode` | none | Chart body (for example a `BarChart` or `LineChart`) | | `title` | Optional | `string` | none | Title text | | `titleExtra` | Optional | `ReactNode` | none | Node rendered immediately after the title text (e.g. an info tooltip); only shown when `title` is set and `header` is not | | `header` | Optional | `ReactNode` | none | Custom header node | | `headerAction` | Optional | `ReactNode` | none | Action rendered on the right side of the header row (e.g. an expand toggle); sits alongside the range selector when both are present | | `legendData` | Optional | `LegendItem[]` | none | Legend entries rendered in the container chrome | | `highlightedValue` | Optional | `object` | none | Highlighted metric (`value`, `unit`, `className`, `style`); highlight chrome renders only when this prop is set and the chart body is visible (not `loading` / not `empty`) | | `rangeSelector` | Optional | `object` | none | Range selector props (`options`, `value`, `onChange`, `className`, `style`, `buttonClassName`) | | `loading` | Optional | `boolean` | none | When `true`, shows a centered `Loader` overlay over the chart area | | `empty` | Optional | `boolean` | none | When `true`, shows empty state | | `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty overlay when `empty` is `true` and `loading` is not `true` | | `minMaxAvg` | Optional | `object` | none | Min / max / avg strings for the summary row | | `timeRange` | Optional | `string` | none | Time range label | | `footer` | Optional | `ReactNode` | none | Footer slot | | `footerClassName` | Optional | `string` | none | Class name on the footer wrapper | | `onToggleDataset` | Optional | `function` | none | Called with dataset index when legend toggles visibility | | `className` | Optional | `string` | none | Root class name from the host app | ### Basic usage ```tsx ``` ## `ChartStatsFooter` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `minMaxAvg` | Optional | `object` | none | Min, max, and average strings shown in the primary row when provided | | `stats` | Optional | `ChartStatsFooterItem[]` | none | Additional stat rows (`label`, `value`) in a columnar grid | | `statsPerColumn` | Optional | `number` | `1` | Number of stat items per column when `stats` is set | | `secondaryLabel` | Optional | `object` | none | Secondary block with `title` and `value` when provided | | `className` | Optional | `string` | none | Root class name from the host app | The component renders **nothing** when `minMaxAvg`, `stats`, and `secondaryLabel` are all absent or empty. ### Basic usage ```tsx ``` ## `DetailLegend` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `items` | Required | `DetailLegendItem[]` | none | Legend rows (`label`, `color`, optional `icon`, `currentValue`, `percentChange`, `hidden`) | | `onToggle` | Optional | `function` | none | Called with `label` and index when a row is toggled | | `className` | Optional | `string` | none | Root class name from the host app | ### Basic usage ```tsx ``` ## `MinMaxAvg` Compact summary row displaying min, max, and average values with consistent MDK label and value styling. Rendered automatically by [`ChartContainer`](#chartcontainer) when its `minMaxAvg` prop is set, and by [`ChartStatsFooter`](#chartstatsfooter) in the same way. Use it directly when you need the summary row outside of a `ChartContainer` or `ChartStatsFooter`. The component renders **nothing** when all three values are absent or empty strings. ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `min` | Optional | `string` | none | Minimum value label (for example `'65 TH/s'`) | | `max` | Optional | `string` | none | Maximum value label | | `avg` | Optional | `string` | none | Average value label | | `className` | Optional | `string` | none | Root class name from the host app | ### `MinMaxAvgValues` type ```tsx type MinMaxAvgValues = Partial<{ min: string max: string avg: string }> ``` `MinMaxAvgValues` is the shape accepted by the `minMaxAvg` prop on both `ChartContainer` and `ChartStatsFooter`. ### Basic usage ```tsx ``` ### Usage with `ChartContainer` ```tsx const stats = computeStats(values) ``` ## Chart utilities Pure functions for building Chart.js data and options. Import from the package root alongside components. ```tsx defaultChartOptions, defaultChartColors, buildBarChartData, buildBarChartOptions, buildChartTooltip, computeStats, } from '@tetherto/mdk-react-devkit/core' ``` | Export | Role | |--------|------| | `defaultChartOptions` | Shared Chart.js defaults used by MDK chart components | | `defaultChartColors` | Default dataset color palette | | `buildBarChartData` | Map MDK bar input into Chart.js `data` | | `buildBarChartOptions` | Build bar chart options (stacking, axes, formatters) | | `buildChartTooltip` | HTML tooltip config for Chart.js | | `computeStats` | Min, max, and average for a numeric array | Types `BarChartInput`, `BarChartSeries`, `BarChartLine`, and `BarChartConstant` are exported from the same package for hook-shaped bar data. ### Hook-shaped bar data App and reporting hooks often return declarative bar input instead of Chart.js `data`. `buildBarChartData` converts that shape into `{ labels, datasets }` for [`BarChart`](/reference/ui/react/core/components/charts#barchart). The pipeline: 1. **Input** (`BarChartInput`): optional `labels`, required `series`, optional `lines` and `constants` for mixed bar/line overlays. 2. **Build** (`buildBarChartData`): returns Chart.js `data` with MDK gradient styling and layout defaults (`barWidth`, `categoryPercentage`, `barPercentage` are optional on the input). 3. **Data labels** (optional): per-series overrides on the input (`formatter`, `anchor`, `align`, `offset`, `font`, `padding`, `display`, `clamp`, `clip`) map to each built dataset’s Chart.js `datalabels` by series index. 4. **Render** (``): pass the built object to the component; pair with `buildBarChartOptions` when you need stacking, axes, or formatters. `BarChartInput` shape | Field | Status | Type | Description | |-------|--------|------|-------------| | `series` | Required | `BarChartSeries[]` | Bar datasets (`label`, `values`, optional `color`, `stack`, `gradient`, bar layout props) | | `labels` | Optional | `string[]` | Category labels; omitted labels are derived from series `values` keys or indices | | `lines` | Optional | `BarChartLine[]` | Line overlays on the same chart | | `constants` | Optional | `BarChartConstant[]` | Horizontal reference lines | Each `BarChartSeries` uses `values` as either `number[]` (positional) or `Record` (keyed by category label). #### Example Hook output to `BarChart` example: ```tsx BarChart, buildBarChartData, ChartContainer, } from '@tetherto/mdk-react-devkit/core' // Typical shape returned by app/reporting data hooks const hookOutput = { labels: ['Q1', 'Q2', 'Q3'], series: [ { label: 'Revenue', values: [4.2, 3.8, 5.1], color: '#72F59E', dataLabels: { formatter: (v: number) => `${v.toFixed(1)}M`, anchor: 'end', align: 'top', }, }, { label: 'OpEx', values: [1.8, 2.0, 1.6], color: '#FFD700', }, ], } const cleanSeries = hookOutput.series.map(({ dataLabels: _dl, ...s }) => s) const base = buildBarChartData({ labels: hookOutput.labels, series: cleanSeries }) const chartData = { labels: base.labels, datasets: base.datasets.map((dataset, i) => { const overrides = hookOutput.series[i]?.dataLabels return overrides ? { ...dataset, datalabels: overrides } : dataset }), } const isEmpty = !hookOutput.series.length || hookOutput.series.every((s) => { const vals = Array.isArray(s.values) ? s.values : Object.values(s.values) return !vals.length || vals.every((v) => v === 0) }) ``` #### Empty and all-zero series Treat bar data as empty when any of the following is true: - `series` is missing or has length `0` - Every series has empty `values` (array or record) - Every numeric value across all series is `0` Prefer `ChartContainer` `empty` or a placeholder instead of rendering a flat chart. After building Chart.js `data`, you can also use [`useChartDataCheck`](/reference/app-toolkit/hooks/components#usechartdatacheck) from `@tetherto/mdk-react-devkit/foundation` with `{ data: chartData }`. # Data display (/reference/ui/react/core/components/data-display) Components for displaying data in cards, tables, badges, tags, and other visual formats. ## Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/core#prerequisites) ## `DataTable` ### Import ```tsx // Types for column definitions DataTableColumnDef, DataTableSortingState, DataTablePaginationState, DataTableRowSelectionState, } from '@tetherto/mdk-react-devkit/core' ``` ### Basic usage ```tsx type Miner = { id: string name: string hashrate: number status: string } const columns: DataTableColumnDef[] = [ { accessorKey: 'name', header: 'Name', }, { accessorKey: 'hashrate', header: 'Hashrate', cell: ({ row }) => `${row.original.hashrate} TH/s`, }, { accessorKey: 'status', header: 'Status', }, ] function MinersTable() { return } ``` ### With sorting and pagination ```tsx const [sorting, setSorting] = useState([]) const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }) ``` ### With row selection ```tsx const [rowSelection, setRowSelection] = useState({}) ``` ### Styling - `.mdk-data-table`: Root container - `.mdk-data-table__header`: Header row - `.mdk-data-table__body`: Table body - `.mdk-data-table__row`: Data row - `.mdk-data-table__cell`: Table cell ## `Card` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `className` | Optional | `string` | none | Additional CSS class | | `onClick` | Optional | `function` | none | Click handler (adds clickable styling) | | `children` | Optional | `ReactNode` | none | Card content | ### Basic usage ```tsx Title Content goes here Actions ``` Default children are automatically wrapped in the body slot: ```tsx Title This content goes to the body automatically ``` ### Clickable card ```tsx navigate('/details')}> Click me Navigates on click ``` ### Styling - `.mdk-card`: Root container - `.mdk-card--clickable`: Applied when `onClick` is provided - `.mdk-card__header`: Header slot - `.mdk-card__body`: Body slot - `.mdk-card__footer`: Footer slot ## `LabeledCard` A generic card container with a header label, an optional navigation link on the label, and a set of layout modifiers. Useful for compact stat or metadata blocks. ### Import ```tsx ``` ### Props All props are optional. | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `label` | Optional | `ReactNode` | none | Header content shown above the card body | | `children` | Optional | `ReactNode` | none | Card body content | | `getNavigateOptions` | Optional | `(label: string) => { href?: string; target?: string }` | none | Returns a link `href`/`target` for the label; only activates when `label` is a plain string | | `isDark` | Optional | `boolean` | `false` | Applies a dark background modifier | | `isFullWidth` | Optional | `boolean` | `false` | Stretches the card to full container width | | `isFullHeight` | Optional | `boolean` | `false` | Stretches the card to full container height | | `isRelative` | Optional | `boolean` | `false` | Sets `position: relative` on the container | | `isScrollable` | Optional | `boolean` | `false` | Enables vertical scroll on the card body | | `hasNoWrap` | Optional | `boolean` | `false` | Prevents content from wrapping | | `hasNoMargin` | Optional | `boolean` | `false` | Removes default margin | | `hasNoBorder` | Optional | `boolean` | `false` | Removes the card border | | `className` | Optional | `string` | none | Additional class for the root element | ### Basic usage ```tsx

Content goes here.

``` ### With a navigation link on the label ```tsx ({ href: '/miners?filter=error' })} > ``` When `label` is the string `'Miners with error'`, an informational tooltip is added automatically, explaining that minor errors not affecting hashrate are excluded. ## `Accordion` ### Import ```tsx Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from '@tetherto/mdk-react-devkit/core' ``` ### `Accordion` props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `title` | Optional | `string` | `''` | Accordion header title | | `isOpened` | Optional | `boolean` | `false` | Initially expanded | | `isRow` | Optional | `boolean` | `false` | Row layout for content | | `unpadded` | Optional | `boolean` | `false` | Remove content padding | | `noBorder` | Optional | `boolean` | `false` | Remove trigger border | | `solidBackground` | Optional | `boolean` | `false` | Solid background color | | `showToggleIcon` | Optional | `boolean` | `true` | Show expand/collapse icon | | `toggleIconPosition` | Optional | `'left' \| 'right'` | `'left'` | Icon position | | `customLabel` | Optional | `ReactNode` | none | Custom label next to title | | `onValueChange` | Optional | `function` | none | Callback when state changes | ### Basic usage ```tsx

This content can be expanded or collapsed.

``` ### With custom label ```tsx Active} showToggleIcon={false} >

All systems operational.

``` ### Multiple items ```tsx Section 1 Content 1 Section 2 Content 2 ``` ### Styling - `.mdk-accordion`: Root container - `.mdk-accordion--solid-background`: Solid background variant - `.mdk-accordion__item`: Individual item - `.mdk-accordion__trigger`: Clickable header - `.mdk-accordion__content`: Collapsible content area ## `Badge` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `count` | Optional | `number` | `0` | Number to display | | `overflowCount` | Optional | `number` | `99` | Max count before showing "99+" | | `showZero` | Optional | `boolean` | `false` | Show badge when count is 0 | | `dot` | Optional | `boolean` | `false` | Show as dot instead of number | | `text` | Optional | `string` | none | Custom text content | | `color` | Optional | [`ColorVariant`](/reference/app-toolkit/ui-devkit/types#colorvariant) | `'primary'` | Badge color | | `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'md'` | Badge size | | `status` | Optional | `'success' \| 'processing' \| 'error' \| 'warning' \| 'default'` | none | Status variant | | `square` | Optional | `boolean` | `false` | Square badge (no border-radius) | | `offset` | Optional | `[number, number]` | `[0, 0]` | Position offset [x, y] | ### Basic usage ```tsx // Number badge on content // Overflow // Shows "99+" ``` ### Dot badge ```tsx ``` ### Status badge ```tsx ``` ### Standalone badge ```tsx ``` ### Styling - `.mdk-badge`: Badge element - `.mdk-badge--{color}`: Color variant - `.mdk-badge--{size}`: Size variant - `.mdk-badge--dot`: Dot variant - `.mdk-badge--status`: Status variant - `.mdk-badge-wrapper`: Wrapper when wrapping content ## `Pagination` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `current` | Optional | `number` | `1` | Current page number | | `total` | Optional | `number` | `0` | Total number of items | | `pageSize` | Optional | `number` | `20` | Items per page | | `pageSizeOptions` | Optional | `number[]` | `[10, 20, 50, 100]` | Page size options | | `showSizeChanger` | Optional | `boolean` | `true` | Show page size dropdown | | `showTotal` | Optional | `boolean` | `false` | Show total count text | | `disabled` | Optional | `boolean` | `false` | Disable pagination | | `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'sm'` | Size variant | | `onChange` | Optional | `(page: number, pageSize: number) => void` | none | Callback when the page number or page size changes | | `onSizeChange` | Optional | `(current: number, size: number) => void` | none | Callback when the page size changes | ### Basic usage ```tsx const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) { setPage(newPage) setPageSize(newSize) }} /> ``` ### With total display ```tsx // Shows "1-20 of 100" ``` ### Styling - `.mdk-pagination`: Root container - `.mdk-pagination__pages`: Page buttons container - `.mdk-pagination__button`: Navigation button - `.mdk-pagination__button--active`: Active page - `.mdk-pagination__total`: Total count text - `.mdk-pagination__size-changer`: Page size select ## `Tag` ### Import ```tsx ``` ### Basic usage ```tsx Default Tag Primary Success handleRemove()}>Removable ``` ## `Avatar` ### Import ```tsx ``` ### Basic usage ```tsx JD ``` ## `SkeletonBlock` ### Import ```tsx ``` ### Basic usage ```tsx // Text skeleton // Circle skeleton // Card skeleton ``` ## `EmptyState` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `description` | Optional | `ReactNode` | none | **Required.** Description text | | `image` | Optional | `'default' \| 'simple' \| ReactNode` | `'default'` | Image/icon to display | | `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant | ### Basic usage ```tsx ``` ### Custom image ```tsx } /> ``` ### Styling - `.mdk-empty-state`: Root container - `.mdk-empty-state--{size}`: Size variant - `.mdk-empty-state__image`: Image container - `.mdk-empty-state__description`: Description text ## `BtcAveragePrice` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `price` | Optional | `number \| null` | none | BTC price in USD; formatted with grouping and no decimal places. Shows `-` when `null`, `undefined`, non-finite, or negative | | `label` | Optional | `string` | `'BTC Average Price'` | Label shown before the price | ### Basic usage ```tsx ``` ## `Typography` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `variant` | Optional | `'heading1' \| 'heading2' \| 'heading3' \| 'body' \| 'secondary' \| 'caption'` | none | Typography variant | | `size` | Optional | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| '3xl' \| '4xl'` | none | Text size | | `weight` | Optional | `'light' \| 'normal' \| 'medium' \| 'semibold' \| 'bold'` | none | Font weight | | `align` | Optional | `TextAlign` | none | Text alignment | | `color` | Optional | `'default' \| 'muted' \| 'primary' \| 'success' \| 'warning' \| 'error'` | none | Text color variant | | `truncate` | Optional | `boolean` | `false` | Truncate text with ellipsis | | `className` | Optional | `string` | none | Custom class name | ### Basic usage ```tsx Operations dashboard Sites Primary site operations are running within expected parameters. Last refreshed 12 seconds ago. Updated 2 minutes ago Site #1 ``` ## `Indicator` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `color` | Optional | `'gray' \| 'slate' \| 'red' \| 'amber' \| 'yellow' \| 'green' \| 'blue' \| 'purple'` | none | Color variant of the indicator dot | | `size` | Optional | `'sm' \| 'md' \| 'lg'` | none | Size variant of the indicator | | `vertical` | Optional | `boolean` | `false` | When `true`, adds extra spacing between child elements and stacks them vertically | | `onClick` | Optional | `function` | none | Click handler | | `children` | Optional | `ReactNode` | none | Label content (text, icons, or multiple elements) | | `className` | Optional | `string` | none | Custom class name | ### Basic usage ```tsx Online Offline Maintenance Running Running 142 ``` ## `CurrencyToggler` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `currencies` | Required | `(string \| CurrencyItem)[]` | none | Currencies to toggle between, as codes or `CurrencyItem` objects | | `value` | Required | `string` | none | Currently selected currency code | | `onChange` | Required | `(currency: string) => void` | none | Called with the newly selected currency code | | `className` | Optional | `string` | none | Custom class name for the root element | ### Basic usage ```tsx ``` ## `ListViewFilter` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `options` | Required | `CascaderOption[]` | none | Cascader options for filtering | | `onChange` | Required | `(selections: CascaderValue[]) => void` | none | Callback when filters change | | `localFilters` | Optional | `LocalFilters` | none | Current filter values as key-value pairs (e.g. `{ status: ['active'] }`) | | `filterKey` | Optional | `string` | none | Key to force re-mounting the Cascader when filters change (resets its internal state) | | `className` | Optional | `string` | none | Custom class name for the filter button | ### Basic usage ```tsx const FILTER_OPTIONS = [ { value: 'type', label: 'Type', children: [ { value: 'Antminer S19XP', label: 'Antminer S19XP' }, { value: 'Avalon A1346', label: 'Avalon A1346' }, ], }, { value: 'status', label: 'Status', children: [ { value: 'active', label: 'Active' }, { value: 'offline', label: 'Offline' }, ], }, ] const [filters, setFilters] = useState({}) setFilters(selectionToFilters(selections))} /> ``` ## `Mosaic` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `template` | Required | `string[] \| string[][]` | none | Grid layout template — area names per row that map child `MosaicItem`s to grid regions | | `children` | Required | `ReactNode` | none | `MosaicItem` elements placed into the named grid areas | | `columns` | Optional | `string \| string[]` | none | Column track sizes (e.g. `'1fr 2fr 1fr'`); defaults to equal columns derived from the template | | `gap` | Optional | `string` | `'12px'` | Gap between grid cells (any CSS length) | | `rowHeight` | Optional | `string` | `'auto'` | Height applied to each grid row (any CSS length) | | `className` | Optional | `string` | none | Custom class name for the root element | ### Basic usage ```tsx Hashrate Active miners Power usage ``` # Errors (/reference/ui/react/core/components/errors) Components for displaying errors, alerts, and handling error states. ## Prerequisites - Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/reference/ui/react/core#prerequisites) ## `CoreAlert` ### Import ```tsx ``` The component is authored as `Alert` in `@tetherto/mdk-react-devkit/core` but re-exported as `CoreAlert` at the package barrel to avoid collisions with other `Alert` primitives (for example Radix `AlertDialog`). ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `type` | Optional | `'success' \| 'info' \| 'warning' \| 'error'` | `'info'` | Alert type | | `title` | Optional | `ReactNode` | none | Main message | | `description` | Optional | `ReactNode` | none | Additional description | | `showIcon` | Optional | `boolean` | `false` | Show type icon | | `icon` | Optional | `ReactNode` | none | Custom icon | | `closable` | Optional | `boolean` | `false` | Show close button | | `onClose` | Optional | `function` | none | Close callback | | `banner` | Optional | `boolean` | `false` | Full-width banner style | | `action` | Optional | `ReactNode` | none | Action element | ### Basic usage ```tsx ``` ### With action ```tsx View Details} /> ``` ### Banner style ```tsx ``` ### Styling - `.mdk-alert`: Root container - `.mdk-alert--{type}`: Type variant - `.mdk-alert--banner`: Banner variant - `.mdk-alert__icon`: Icon container - `.mdk-alert__content`: Content wrapper - `.mdk-alert__title`: Title text - `.mdk-alert__description`: Description text - `.mdk-alert__action`: Action container - `.mdk-alert__close`: Close button ## `ErrorBoundary` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `fallback` | Optional | `ReactNode` | none | Custom fallback UI | | `componentName` | Optional | `string` | none | Component name for error display | | `onError` | Optional | `function` | none | Error callback | | `children` | Required | `ReactNode` | none | Children to wrap | ### Basic usage ```tsx Something went wrong}> ``` ### With component name ```tsx console.error(error)} > ``` ### Higher-order component ```tsx const SafeChart = withErrorBoundary( Chart, 'Chart', (error) => logError(error) ) ``` #### Styling - `.mdk-error-boundary`: Root container - `.mdk-error-boundary__title`: Error title - `.mdk-error-boundary__message`: Error message - `.mdk-error-boundary__details`: Expandable details - `.mdk-error-boundary__stack`: Stack trace ## `ErrorCard` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `error` | Required | `string` | none | Error message (supports `\n` for line breaks) | | `title` | Optional | `string` | `'Errors'` | Title displayed above the error message | | `variant` | Optional | `'card' \| 'inline'` | `'card'` | Display variant — `card` shows a bordered container, `inline` shows flat text | | `className` | Optional | `string` | none | Additional CSS class | ### Basic usage ```tsx ``` ### Styling - `.mdk-error-card`: Root container - `.mdk-error-card--{variant}`: Variant modifier (`card` / `inline`) - `.mdk-error-card__title`: Title text - `.mdk-error-card__message`: Message text ## `NotFoundPage` ### Import ```tsx ``` ### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `onGoHome` | Optional | `VoidFunction` | none | Callback fired when the "Go Home" button is clicked (button only renders when provided) | | `title` | Optional | `string` | none | Page title | | `message` | Optional | `string` | none | Message displayed below the title | | `className` | Optional | `string` | none | Additional CSS class | ### Basic usage ```tsx navigate('/')} /> navigate('/')} /> ``` ### Styling - `.mdk-not-found-page`: Root container - `.mdk-not-found-page__title`: Title text - `.mdk-not-found-page__message`: Message text - `.mdk-not-found-page__button`: Go Home button # Form components (/reference/ui/react/core/components/forms) Form components handle user input and form submission. All components are built with accessibility in mind and support keyboard navigation. The category splits into three groups: - [Controls](#controls): raw inputs you can drop in standalone or compose inside `
` - [Composition](/reference/ui/react/core/components/forms/composition): the `` provider and the low-level building blocks (`FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, `FormMessage`) - [Prebuilt fields](/reference/ui/react/core/components/forms/fields): one-tag wrappers that bind a control to React hook form ## Prerequisites Before using these components, complete the [@tetherto/mdk-react-devkit installation](/reference/ui/react/core#prerequisites). ## Controls Standalone form controls. Manage `value` and `onChange`, or compose them inside [``](/reference/ui/react/core/components/forms/composition#form) with [`FormField`](/reference/ui/react/core/components/forms/composition#formfield). ### `ActionButton` Triggers an action after **confirmation** in either an inline **popover** (default) or a modal **dialog**. Set **variant** so the trigger [`Button`](#button) matches severity (for example `danger` for destructive flows). After the user confirms, run your side effects there. Consider showing feedback with [`Toast`](/reference/ui/react/core/components/overlays#toast) and **`Toaster`** from the same Toast setup as in [`overlays`](/reference/ui/react/core/components/overlays). This is the pattern the [demo app(../quickstart) applies. #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `label` | Optional | `string` | none | Trigger button label | | `variant` | Optional | `'primary' \| 'secondary' \| 'danger'` | `'secondary'` | Visual variant of the trigger [`Button`](#button) | | `loading` | Optional | `boolean` | `false` | Loading state on the trigger | | `disabled` | Optional | `boolean` | `false` | Disable the trigger | | `className` | Optional | `string` | none | Class on the trigger | | `mode` | Optional | `'popover' \| 'dialog'` | `'popover'` | **`popover`**: confirmation UI in an anchored popover. **`dialog`**: confirmation UI in a modal [`Dialog`](/reference/ui/react/core/components/overlays#dialog). | | `confirmation` | Required | `ActionButtonConfirmation` | none | title, body copy, and confirm/cancel handlers. See [Confirmation object](#confirmation-object). | #### Confirmation object Nested inside `confirmation`: | Field | Type | Description | |-------|------|-------------| | `title` | `string` | Heading shown in the confirmation UI | | `description` | `ReactNode` | Optional body (text or JSX) | | `onConfirm` | `() => void` | Called when the user confirms | | `onCancel` | `() => void` | Called when the user cancels | | `confirmLabel` | `string` | Optional confirm button label (defaults vary by `mode`) | | `cancelLabel` | `string` | Optional cancel button label (defaults to **Cancel**) | | `icon` | `ReactNode` | Optional icon in **popover** header (popover mode only; dialog layout uses title and body) | #### Basic usage (popover) Default `mode` is **`popover`**: compact confirmation next to the trigger. ```tsx { void restartService() }, }} /> ``` #### Dialog mode Set **`mode="dialog"`** when you want a full modal confirmation (for example irreversible or high-impact actions). ```tsx { void factoryReset() }, }} /> ``` ### `Button` #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `variant` | Optional | `'primary' \| 'secondary' \| 'danger' \| 'tertiary' \| 'link' \| 'icon' \| 'outline' \| 'ghost'` | `'secondary'` | Visual variant | | `size` | Optional | `'sm' \| 'md' \| 'lg'` | none | Button size | | `loading` | Optional | `boolean` | `false` | Show loading spinner | | `fullWidth` | Optional | `boolean` | `false` | Expand to container width | | `icon` | Optional | `ReactNode` | none | Icon element | | `iconPosition` | Optional | `'left' \| 'right'` | `'left'` | Icon placement | | `contentClassName` | Optional | `string` | none | Class names applied to the inner content wrapper | | `disabled` | Optional | `boolean` | `false` | Disable interaction | #### Basic usage ```tsx ``` #### With icon ```tsx ``` #### Loading state ```tsx ``` #### Styling - `.mdk-button`: Root element - `.mdk-button--variant-{variant}`: Variant modifier - `.mdk-button--size-{size}`: Size modifier - `.mdk-button--full-width`: Full width modifier - `.mdk-button--loading`: Loading state ### `Cascader` #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `options` | Required | `CascaderOption[]` | none | Hierarchical options; parent options with children render in the left panel | | `value` | Optional | `CascaderValue \| CascaderValue[]` | none | Current selected value(s) | | `onChange` | Optional | `(value: CascaderValue \| CascaderValue[] \| null) => void` | none | Callback when the selection changes | | `multiple` | Optional | `boolean` | `false` | Enable multiple selection (shows checkboxes; selected items render as tags) | | `placeholder` | Optional | `string` | none | Placeholder text shown when no selections are made | | `disabled` | Optional | `boolean` | `false` | Disable the entire cascader | | `dropdownClassName` | Optional | `string` | none | Custom class name for the dropdown panels container | | `className` | Optional | `string` | none | Custom class name for the root element | #### Basic usage ```tsx ``` ### `Checkbox` #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `size` | Optional | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Checkbox size | | `color` | Optional | `'default' \| 'primary' \| 'success' \| 'warning' \| 'error'` | `'primary'` | Color when checked | | `radius` | Optional | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | `'none'` | Border radius | | `checked` | Optional | `boolean \| 'indeterminate'` | none | Checked state (controlled) | | `onCheckedChange` | Optional | `function` | none | Change callback | #### Basic usage ```tsx ``` #### Sizes and colors ```tsx ``` #### Styling - `.mdk-checkbox`: Root element - `.mdk-checkbox--{size}`: Size modifier - `.mdk-checkbox--{color}`: Color modifier - `.mdk-checkbox__indicator`: Check mark container ### `DatePicker` #### Import ```tsx ``` #### Basic usage ```tsx ``` ### `DateRangePicker` #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `selected` | Optional | `DateRange` | none | Selected `{ from, to }` range (controlled) | | `onSelect` | Optional | `(range: DateRange \| undefined) => void` | none | Called when the user applies a range | | `placeholder` | Optional | `string` | `'Pick a date range'` | Trigger text when no range is selected | | `dateFormat` | Optional | `string` | `'MM/dd/yyyy'` | `date-fns` format used in the trigger label | | `disabled` | Optional | `boolean` | `false` | Disable interaction | | `showPresets` | Optional | `boolean` | `true` | Show the default preset buttons for the last 7, 14, 30, and 90 days | | `presets` | Optional | `PresetItem[]` | none | Override the default preset list | | `allowFutureDates` | Optional | `boolean` | `false` | When `false`, dates after today are disabled | | `triggerClassName` | Optional | `string` | none | Extra classes for the trigger button | | `calendarClassName` | Optional | `string` | none | Extra classes for the calendar | | `modalClassName` | Optional | `string` | none | Extra classes for the popover modal | Any other [`react-day-picker` props](https://daypicker.dev/) (excluding `mode` and `selected`) are forwarded to the underlying calendar. #### Basic usage ```tsx const [range, setRange] = useState() ``` #### Custom presets ```tsx const presets: PresetItem[] = [ { label: 'This week', value: { from: startOfWeek(new Date()), to: new Date() } }, { label: 'This month', value: { from: startOfMonth(new Date()), to: new Date() } }, ] ``` #### Allowing future dates ```tsx ``` #### Styling - `.mdk-date-picker__trigger`: Trigger button - `.mdk-date-picker__modal`: Popover modal container - `.mdk-date-picker__calendar`: Calendar grid - `.mdk-date-picker__summary`: Selected-range summary block - `.mdk-date-picker__presets`: Preset button row ### `Input` #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `label` | Optional | `string` | none | Visible label for the field | | `variant` | Optional | `'default' \| 'search'` | `'default'` | Input variant | | `size` | Optional | `'default' \| 'medium'` | `'default'` | Input size | | `error` | Optional | `string` | none | Error message (shows red border) | | `prefix` | Optional | `ReactNode` | none | Element before input | | `suffix` | Optional | `ReactNode` | none | Element after input | | `wrapperClassName` | Optional | `string` | none | Wrapper element class | #### Basic usage ```tsx ``` #### With prefix/suffix ```tsx } placeholder="Username" /> ``` #### Error state ```tsx setMac(e.target.value)} /> ``` #### Styling - `.mdk-input`: Input element - `.mdk-input__wrapper`: Wrapper container - `.mdk-input__wrapper--error`: Error state - `.mdk-input__label`: Label element - `.mdk-input__prefix`: Prefix element - `.mdk-input__suffix`: Suffix element - `.mdk-input__error`: Error message ### `Label` #### Import ```tsx ``` #### Basic usage ```tsx ``` ### `MultiSelect` #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `options` | Required | `MultiSelectOption[]` | none | Selectable options (`{ value, label }`) | | `value` | Optional | `string[]` | none | Controlled selected values; omit to use `defaultValue` for uncontrolled mode | | `defaultValue` | Optional | `string[]` | none | Initial values for uncontrolled mode; ignored when `value` is provided | | `onValueChange` | Optional | `(next: string[]) => void` | none | Called with the updated selection | | `placeholder` | Optional | `ReactNode` | none | Trigger placeholder shown when nothing is selected | | `emptyMessage` | Optional | `ReactNode` | none | Rendered inside the popover when `options` is empty | | `maxSelectedDisplay` | Optional | `number` | none | Max selected chips shown in the trigger before collapsing the rest into a "+N more" badge | | `disabled` | Optional | `boolean` | `false` | Disable the control | | `size` | Optional | `MultiSelectSize` | none | Size variant | | `variant` | Optional | `MultiSelectVariant` | none | Visual variant | | `aria-label` | Optional | `string` | none | Accessible label applied to the trigger button | | `className` | Optional | `string` | none | Custom class name for the root element | | `contentClassName` | Optional | `string` | none | Custom class name for the popover content panel | #### Basic usage ```tsx const MINER_TYPES: MultiSelectOption[] = [ { value: 'miner-am-s19xp', label: 'Antminer S19XP' }, { value: 'miner-wm-m56s', label: 'WhatsMiner M56S' }, { value: 'miner-av-a1346', label: 'Avalon A1346' }, ] function MinerTypeFilter() { const [selected, setSelected] = useState([]) return ( ) } ``` ### `Radio` #### Radio composition parts | Part | Role | |------|------| | `RadioGroup` | Root that holds the selected value and calls `onValueChange` when it changes. | | `Radio` | Default circular radio item; use `label` or children for visible text. | | `RadioCard` | Card-styled option for horizontal or compact layouts; uses the same item primitive as `Radio`. | #### Import ```tsx ``` #### `Radio` props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `label` | Optional | `string` | none | Label text (or use children for custom content) | | `size` | Optional | `'sm' \| 'md' \| 'lg'` | none | Size variant of the radio | | `color` | Optional | `'default' \| 'primary' \| 'success' \| 'warning' \| 'error'` | none | Color variant when checked | | `radius` | Optional | `BorderRadius` | none | Border radius variant (`full` makes it circular) | | `children` | Optional | `ReactNode` | none | Custom content (takes precedence over `label`) | | `indicatorClassName` | Optional | `string` | none | Custom class name for the indicator element | | `className` | Optional | `string` | none | Custom class name for the root element | #### Basic usage ```tsx Small Medium Large ``` **RadioCard** suits card-style, horizontal groups: ```tsx ``` ### `Select` #### Import ```tsx Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, } from '@tetherto/mdk-react-devkit/core' ``` #### `Select` props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `allowClear` | Optional | `boolean` | `false` | Show a clear button when a value is selected | #### `SelectTrigger` props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'lg'` | Trigger size | | `variant` | Optional | `'default' \| 'colored'` | `'default'` | Visual variant | | `color` | Optional | `string` | none | Custom color for colored variant (hex) | #### Basic usage ```tsx ``` #### With groups ```tsx ``` #### Colored variant ```tsx ``` #### Styling - `.mdk-select__trigger`: Trigger button - `.mdk-select__trigger--{size}`: Size modifier - `.mdk-select__trigger--colored`: Colored variant - `.mdk-select__content`: Dropdown content - `.mdk-select__item`: Option item ### `Switch` #### Import ```tsx ``` #### Props | Prop | Status | Type | Default | Description | |------|--------|------|---------|-------------| | `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'md'` | Switch size | | `color` | Optional | `'default' \| 'primary' \| 'success' \| 'warning' \| 'error'` | `'default'` | Color when checked | | `radius` | Optional | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | `'none'` | Border radius | | `checked` | Required | `boolean` | none | Checked state (controlled) | | `onCheckedChange` | Required | `function` | none | Change callback | #### Basic usage ```tsx ``` #### Sizes and colors ```tsx ``` #### Styling - `.mdk-switch`: Root element - `.mdk-switch--{size}`: Size modifier - `.mdk-switch--{color}`: Color modifier - `.mdk-switch__thumb`: Toggle thumb ### `TagInput` #### Import ```tsx ``` #### Basic usage ```tsx ``` ### `TextArea` #### Import ```tsx ``` #### Basic usage ```tsx